Top-level Symbol() calls in router_resource.ts lacked /* @__PURE__ */
annotations, preventing bundlers like esbuild and Rollup from tree-shaking
the module and its dependencies when provideRouter() is used without
withRouterResources().
Fixes#70696
Previously, rollback recovery waited on hasValueOrResolved(), which checked whether the resource already had a value. This caused resources with values (e.g. defaultValue, existing values from prior navigations, or streamed emissions) to unfreeze prematurely while still in a loading state during rollback.
This change updates the rollback recovery check in the transactional snapshot effect to be solely determined by !source.isLoading().
providing the snapshot is a bit incompatible with how things are meant to work here.
The snapshot is only accurate during the setup and would be out of date
on followup navigations
Previously, blocking router resources waited on hasValueOrResolved(), which checked whether the resource already had a value. This caused blocking resources with initial values (e.g. defaultValue), streamed early emissions, or existing values during route reloads to resolve prematurely while still in a loading state.
This change updates the blocking resolution check to be solely determined by !underlyingRes.isLoading().
Replaces the remaining hand-rolled deferred promise implementations
with the native `Promise.withResolvers()` API and removes the now
unused helper and import.
Follow-up to #69739.
When a route is detached for `RouteReuseStrategy`, its component and child
`RouterOutlet`s stay alive and keep referencing the `ChildrenOutletContexts`
they were created with. `detachAndStoreRouteSubtree` used to call
`onOutletDeactivated()`, which swaps that object's Map for an empty one, so
after re-attaching, the inner outlet and the router read from two different
context trees and deeper child routes (e.g. an `edit` route under a reused
list) never rendered.
Now, on detach: take the child contexts map as-is (destruction still calls
`onOutletDeactivated()` to prune) and give the `OutletContext` a fresh
`ChildrenOutletContexts`. This keeps the detached component rendering from
its stored map once re-attached, and prevents whatever activates next in
the same parent outlet (e.g. a sibling tab) from mutating or wiping that
stored map.
Fixes#57285
Removes experimental tags and stabilizes withAutoCleanupInjectors, AutoCleanupInjectorsFeature,
and RouteReuseStrategy cleanup methods, while re-exporting withExperimentalAutoCleanupInjectors as deprecated.
Preserve hasUAVisualTransition through Location and the Router navigation pipeline. This prevents withViewTransitions from starting an author transition after the browser has already performed one, including across redirects and when using experimental platform navigation.
Introduces support for route-level resources via the `resources` property on route definitions, enabled with `withRouterResources()`.
Router resources provide a reactive, signal-based alternative to resolvers, allowing routes to declare resources tied to route parameters and route lifecycle:
- The `resources` function executes during navigation transitions within an injection context scoped to the route (`_localInjector`).
- It receives a `ResourceContext` containing signals for `params`, `queryParams`, `fragment`, and `data`, alongside the static `snapshot`.
- For newly created routes, `resources` runs once and attaches to `ActivatedRoute.resources`. For reused routes, parameter signals update reactively to trigger new data fetches while keeping resource references stable.
- Wrapped resources (`routerResource`) provide transactional stability: snapshots are frozen during active navigations to prevent UI jitter, unfreezing on `NavigationEnd`.
- On cancelled navigations or errors, rollback recovery retains the frozen snapshot until reverted signals settle, avoiding flashes of loading state. Manual reloads are rejected while frozen.
- Local injectors are automatically cleaned up if navigations are cancelled or rolled back.
Note that this commit only implements non-blocking resources (marked with `nonBlocking()`), deferring blocking resource resolution to future work to keep the initial changeset smaller and less complex.
PR Close#70211
Activated Route Injector feature was developed for resources. This moves the internal
naming (files and vars) to reflect this now that we have landed the core bits of plumbing
and are ready to implement router resources.
PR Close#70211
When a componentless parent route has children rendered into a named outlet
(e.g. `outlet: 'inner'`), the `canDeactivate` guard received `null` as the
component argument instead of the actual component instance.
The bug was in `deactivateRouteAndItsChildren`: for componentless routes,
each child was passed the same `context` inherited from the parent lookup,
which was `null` when the parent component only registered a named outlet.
The children's actual outlet contexts were never consulted.
The fix adds a `parentContexts: ChildrenOutletContexts | null` parameter so
that for componentless routes, each child is looked up by its own outlet name
in `parentContexts` (e.g. `parentContexts.getContext('inner')`). For
component routes, `context.children` is passed as the new `parentContexts`
on recursion, ensuring correct context resolution across component boundaries.
This re-addresses #34614. A previous fix (#36302) was reverted because it
passed `parentContexts` unchanged through component boundaries; this fix
updates `parentContexts` to `context.children` when descending into a
component route, preventing the wrong contexts from propagating into deeper
componentless levels.
Fixes#34614
We keep getting PRs that target single usages of `hasOwnProperty` and we have ~100 of them. These changes aim to address the issue centrally by swapping out all the instances and adding a lint rule against introducing new ones.
This commit adds an implementation of a router resource (not currently exposed for public use) which
defines the behavior of a resource dependent on the Router navigation lifecycle.
Preserve createUrlTree command semantics, including custom serializer inputs, while keeping the single-leading-slash guarantee at the default serialization boundary.
Expand coverage for command forms, public UrlTree values, secondary outlets, and preserved query parameters and fragments.
Fixes#69700
This allows developers to throw a `RedirectCommand` directly from guards and resolvers to trigger a redirect.
The primary benefit is that we no longer need to pollute the return type of functions that redirect. For example, a deeply nested helper function or a resolver can now simply throw a `RedirectCommand` to short-circuit and redirect, instead of having to return the `UrlTree` or `RedirectCommand` all the way up the call stack.
This aligns with prior art in other modern framework routers (such as Next.js, Remix, and SvelteKit), which commonly use thrown exceptions or special redirect responses to abort execution and trigger immediate redirection.
Adds build-time validation to catch broken, stale, or miscased internal documentation links in both JSDoc and markdown, including `/api/` and `/guide/` URLs and their fragments. Updates the documentation pipeline to share the canonical route manifest, ensuring that all references are checked against the current navigation structure.
`parseQueryParam` and the AngularJS-compat `parseKeyValue` accumulate query params into a plain object and check key presence with `obj.hasOwnProperty`, so a `hasOwnProperty` query key clobbers the method and the next lookup throws `TypeError`. Switch both to `Object.hasOwn`, which can't be shadowed by a query key.
Updates router integration tests to use the `currentNavigation()` method instead of deprecated `getCurrentNavigation`.
Also replaces direct `setTimeout` calls with the `timeout()` utility function.
Outlet maps are keyed by names read verbatim from the url, so a name like
`__proto__` (e.g. `/one(__proto__:two)`) is assigned through the inherited
`__proto__` setter instead of creating an outlet. This drops the outlet and
mutates the map's prototype, and throws under Node's `--disable-proto=throw`.
Build these outlet maps with `Object.create(null)` so `__proto__` is treated as
an ordinary key. Covers `parseParens` and `squashSegmentGroup` in url_tree.ts,
`createSegmentGroup` in apply_redirects.ts, and `replaceSegment` and
`updateSegmentGroupChildren` in create_url_tree.ts.
The export statement was incorrectly placed inside the JSDoc comment block,
and there was a stray text fragment "nstead." from the deprecation message.
This moves the export statement outside the comment and removes the stray text.
Without this change, components that use RouterLinkActive in multiple
contexts (e.g. both a navigation menu and body content) are forced to
branch the template for every conditional input:
@if (activeClass) {
<a [routerLink]="href" [routerLinkActive]="activeClass"
[routerLinkActiveOptions]="activeOptions"
[ariaCurrentWhenActive]="ariaCurrent">
<ng-content />
</a>
} @else {
<a [routerLink]="href"><ng-content /></a>
}
Every additional input multiplies the branching, and each @if/@else
injects unwanted comment nodes into the DOM. There is no way to
conditionally attach a directive in Angular templates, making imperative
TypeScript instantiation the only alternative.
Accepting null/undefined collapses this to a single template branch:
<a [routerLink]="href"
[routerLinkActive]="activeClass"
[routerLinkActiveOptions]="activeOptions"
[ariaCurrentWhenActive]="ariaCurrent">
<ng-content />
</a>
When activeClass is undefined (e.g. in content areas), the directive
stays mounted but applies no CSS classes. When it is a string (e.g. in
the navigation), normal active-class behavior applies — no branching, no
extra DOM nodes, no TypeScript workarounds.
- `routerLinkActive`: null/undefined now sets an empty class list.
- `routerLinkActiveOptions`: null and undefined are treated differently:
- undefined → falls back to the default subset match ("not set")
- null → explicit opt-out, link is never considered active
Closes#66233
Add handling in navigation for creating and destroying injectors scoped
to `ActivatedRoute` life.
The code for creating the injectors is certainly more complicated
than it _could_ be since there's no actual feature built around this yet.
Keeps as much implementation code tree-shakeable as possible:
Raw size: +764 bytes
Gzipped size: +182 bytes
Previously, `NavigationStateManager` relied on string-based comparisons and `.substring()` to match `NavigateEvent` URLs against internal router transitions or the application root boundary. This was brittle against trailing slashes, query parameter order variations, and sibling application URLs.
This commit updates the logic to:
- Use the native `URL` object to strictly compare `origin` and `pathname` for `appRootURL` boundaries.
- Sort `searchParams` and use `Location.stripTrailingSlash()` to robustly compare the router destination against the event destination.
- Pre-compute and store `appRootUrl` as a `URL` object to avoid redundant parsing on every navigation.
Split the `ng` global interface into two interfaces:
* `ExternalCoreGlobalUtils` includes all the functionality which has been shipped in a long-lived Angular version externally and which is subject to the versioning constraints described above.
* `InternalCoreGlobalUtils` includes internal-only functionality which has **not** been shipped in a long-lived Angular version.
This split means that all APIs in `InternalCoreGlobalUtils` can be iterated and evolved at a much faster pace. Angular DevTools can support those features, and we can make breaking changes more-or-less whenever we want. The downside is that external Angular developers cannot take advantage of those APIs or else we would be subject to the same versioning constraint we're trying to avoid here.
This means we can use `InternalCoreGlobalUtils` as a kind of "beta" channel for new DevTools APIs. Once that functionality is validated and the design is stabilized, the feature can be moved into `ExternalCoreGlobalUtils` and made available for external Angular developers when we're ready to commit to the long-lived version constraint. This will hopefully help us strike a better balance between iterating on new APIs quickly and maintaining stable APIs for external Angular users.
Update the minimum supported Node.js versions for v22 and v24. Specifically, the minimum supported version for Node.js v22 is bumped to v22.22.3, and for v24 it is bumped to v24.15.0. This ensures compatibility with newer runtime versions and coordinates ranges across monorepo packages.
Replace specific file patches applied during google3 sync with generic comment-based mechanisms.
By adding `// g3-only` prefix comments to g3-specific exports and declarations, and appending `// 3p-only` context to `@internal` tags, we enable generic tooling to handle these modifications during the sync process.
Additionally, wrap 3rd-party-only imports and exports (which should be stripped in google3) with `// 3p-only-start` and `// 3p-only-end` comment markers.
This reduces the need for maintaining custom file-specific patches in google3.
Also, add a comprehensive guide to these sync comment markers in `contributing-docs/google-markers.md` to assist external contributors.
Specifically:
- Add `// 3p-only` context to `@internal` in `directives.ts` for `foreignImports` and `deferredImports`.
- Add `// g3-only` commented exports in `core.ts`.
- Add `// g3-only-start`/`// g3-only-end` commented global declaration block in `zone.ts`.
- Wrap 3p-only imports in `fake_navigation.ts` with `// 3p-only-start` and `// 3p-only-end`.
- Wrap 3p-only exports in `compiler-cli/index.ts` with `// 3p-only-start` and `// 3p-only-end`.
- Add `// g3-only` and `// 3p-only` markers to `shared.ts` for `setDisabledStateDefault` configuration.
- Add `// g3-only` and `// 3p-only` markers to `feature_detection.ts` for semver dependency.
- Add `// g3-only` and `// 3p-only` markers to `domino_adapter.ts` for domino import path.
- Add `// 3p-only` marker to `ng_dev_mode` import in `event_dispatcher.ts`.
- Add `// g3-only` and `// 3p-only` markers to `MOUSE_SPECIAL_SUPPORT` in `event_contract_defines.ts`.
- Add `// g3-only` and `// 3p-only` markers to `BrowserModule` imports in `module.ts` (animations) and `browser.ts` (testing).
- Add `// 3p-only` marker to `goog.d.ts` reference tags in `util.ts` (platform-browser), `types.d.ts`, `ng_i18n_closure_mode.ts`, `tokens.ts`, and `global_utils.ts`.
- Wrap `Default` enum value of `ChangeDetectionStrategy` in `constants.ts` with `// 3p-only-start` and `// 3p-only-end`.
- Add `// g3-only` and `// 3p-only` markers to `LEGACY_OPTIONAL_CHAINING_DEFAULT` in `legacy_optional_chaining_default.ts` and `legacyOptionalChaining` in `directive.ts`.
- Add `// g3-only` and `// 3p-only` markers to `DEFAULT_PARAMS_INHERITANCE_STRATEGY` in `router_state.ts`.
- Add `// g3-only` and `// 3p-only` (and block variants) markers to `@mcp-b/webmcp-types` imports in `declare_tool.ts`, `provide_tools.ts`, and `types.ts`.
- Add `contributing-docs/google-markers.md` guide.
TAG=agy
CONV=cd09a4f3-869a-4f41-949b-c91f1b8f1c51
When scrollPositionRestoration is enabled and the app hydrates an
SSR-rendered page, RouterScroller was unconditionally scrolling the
viewport to [0, 0] on the first imperative navigation. This discards
any scroll position the user established while the server-rendered
page was loading.
Fix by injecting IS_HYDRATION_DOM_REUSE_ENABLED into RouterScroller
and suppressing the scroll-to-top for the initial navigation only.
Subsequent navigations are unaffected.
Closes#64578
The title property in Router type already requires that the return type of a resolved
title is a string. The type was looser here only because 'data' values are typed
as 'any'.
BREAKING CHANGE: The return type for `TitleStrategy.getResolvedTitleForRoute`
was previously 'any' while the actual return type could only be either `string`
or `undefined`. The return type now reflects the possible values correctly.
Code that reads the value may need to be adjusted.
This change moves `RouterState` creation to _before_ the `afterPreactivation` step,
which is the step that pauses until bootstrap listeners are complete. It is used for
'enabled blocking' initial navigation and destructive hydration. After this stage,
activation is expected to be (more or less) synchronous.
More importantly than above (since enabled blocking and destructive hydration are
essentially deprecated), this also oves the state creation before the view transition
creation.
These are done to accomodate features in the future that would depend on the RouterState
(e.g. ones which need to know which `ActivatedRoute` instances are new and which are reused).
These features may include additional async blocks/waits, which should not happen after view
transition creation (which freezes the UI until resolved).
Updates the supported Node.js engine versions to include Node.js 26.
This allows running the CLI on Node.js 26.0.0 and above while continuing to support active LTS versions.
"AngularJS" is the official product name for the v1.x line and is
written as a single word. A few places in the docs and package
READMEs used "Angular JS" with a space. This normalizes those
references to the canonical spelling.
Introduce a new configuration option `unmatchedInputBehavior` to the `componentInputBinding` feature. This option allows users to configure the behavior when a component input is not matched by any key in the router data.
The available values are:
- 'alwaysUndefined': (Default) Always binds undefined to unmatched inputs.
- 'undefinedIfStale': Binds undefined only if the input was previously available in the router data for the active route in the outlet.
This feature addresses concerns raised in #63835 and #52946 regarding the retention of default values for inputs that were never targeted by the router, while still ensuring that stale data is cleared when a parameter is removed.
ex:
```
await injectAsync(() => import('./test_service'))
```
We'll be reusing the features that were already used by the router to support components lazy-loading.
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
Set the default value of paramsInheritanceStrategy to 'always'. This change ensures that route parameters are inherited from parent routes by default, which is the behavior most users expect. It simplifies routing configuration for the majority of use cases.
This change aligns Angular with other popular routing systems where child routes automatically have access to parent parameters:
- React Router: useParams() includes parent params.
- Vue Router: $route.params includes parent params.
- Next.js: params are passed to nested layouts and pages.
- TanStack Router: useParams() includes parent params with full type safety.
BREAKING CHANGE: paramsInheritanceStrategy now defaults to 'always'
The default value of paramsInheritanceStrategy has been changed from 'emptyOnly' to 'always'. This means that route parameters are inherited from all parent routes by default. To restore the previous behavior, set paramsInheritanceStrategy to 'emptyOnly' in your router configuration.
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