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.
(cherry picked from commit fe721868a6)
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.
(cherry picked from commit d069c55ab4)
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.
(cherry picked from commit 861d37e669)
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
(cherry picked from commit 8ec0d1eee8)
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.
(cherry picked from commit ad37f52c12)
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).
(cherry picked from commit 1f287b9ba7)
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
Creating a platform and bootstrapping an application might register more events than just what the router expects (and an event will be added to platform creation in this PR). This test shouldn't be so strict about it.
it was only optional to avoid a breaking change in a minor
BREAKING CHANGE: The `currentSnapshot` parameter in `CanMatchFn` and the `canMatch` method of the `CanMatch` interface is now required. While this was already the behavior of the Router at runtime, existing class implementations of `CanMatch` must now include the third argument to satisfy the interface.
PR Close#67452
The `split` helper function in `packages/router/src/utils/config_matching.ts` was blind to the current outlet being processed. When encountering an empty path named outlet in the config, it would assume it needed to pull it in as a synthetic empty group, even if we were already in the process of resolving that very outlet!
When navigating to `/(secondary:component-copy)` with this config:
```typescript
{
path: '',
component: MainLayout,
children: [
{ path: '', outlet: 'secondary', component: SecondaryComponent, children: [{path: 'component-copy'}] }
]
}
```
The router uses `MainLayout` as a pass-through and calls `split` on its children with segments `['component-copy']`.
`split` uses the `containsEmptyPathMatchesWithNamedOutlets` helper to determine if there are any candidate empty path named outlets to pull in. Because of this, it sees `{ path: '', outlet: 'secondary' }` and says: "Ah, an empty path named outlet! I must pull it in!"
Rather than falling through to standard segment matching, it returns `UrlSegmentGroup(segments: [], children: {secondary: emptyGroup})`.
The router then tries to process `primary` (with `[]` segments) and fails because the config only has `secondary`. It also tries to process `secondary` with the `emptyGroup`. While `{ path: '', outlet: 'secondary' }` matches the empty group, its child `{ path: 'component-copy' }` fails to match because the `emptyGroup` has no segments! So both branches fail, resulting in a `NoMatch` error for the entire navigation!
Pulling in empty path named outlets IS desired when they act as siblings to segments we are matching. This has worked before and continues to work!
```typescript
{
path: 'a',
children: [
{ path: 'b', component: ComponentB },
{ path: '', component: ComponentC, outlet: 'aux' }
]
}
```
When navigating to `a/b`, `split` sees segments `['b']` and the `aux` empty path. It pulls in `aux` so it gets instantiated alongside `b`. This is correct!
If we have a named outlet with a non-empty path under an empty path parent:
```typescript
{
path: '',
component: MainLayout,
children: [
{ path: 'component-copy', outlet: 'secondary', component: ComponentE }
]
}
```
When we navigate to `/(secondary:component-copy)`:
- `split` uses `containsEmptyPathMatchesWithNamedOutlets` to see if there are any empty path named outlets. Since it only sees `path: 'component-copy'`, it returns `false`.
- It falls through to standard segment matching, which finds `component-copy` in the segments array and activates it flawlessly!
This worked perfectly before the fix because it didn't use `containsEmptyPathMatchesWithNamedOutlets`.
The fix passes the **current active outlet context** into `split`. If `split` finds an empty path named outlet that matches the outlet we are already processing, it ignores it as a pull-in candidate.
When evaluating `MainLayout` children for `secondary`:
- URL Segments left to process: `['component-copy']`
- Current Outlet: `secondary`
- `childConfig`: `[{ path: '', outlet: 'secondary' }]`
Previously, `split` saw the empty path and pulled it in as a synthetic empty group, breaking matching. Now, since `getOutlet(r) === outlet` (both are `secondary`), the fix ignores it. Instead of returning empty segments, it **falls through to standard segment matching**, which successfully find the `component-copy` segment!
When evaluating `ComponentA` children for `primary`:
- URL Segments left to process: `['b']`
- Current Outlet: `primary`
- `childConfig`: `[{ path: 'b' }, { path: '', outlet: 'aux' }]`
Since `getOutlet(aux) !== primary`, the fix **does not ignore it**. `split` pulls in `aux: emptyGroup` as a sibling, instantiating `ComponentC` alongside `ComponentB`. This preserves correct behavior for auxiliary outlets!
fixes#67708
Add `ComponentInputBindingOptions` which is used with `withComponentInputBinding` and `bindToComponentInputs`
Can set which sources to bind as follows:
* queryParams
* params
* data
feat(router): Add `options` optional parameter for `withComponentInputBinding`
Add missing ternary operator for queryParams
This updates the state manager to allow intercepting and deferring commits of traversal navigations.
The issues that were encountered in the past appear to be resolved in Chrome.
The behavior of redirect is still undefined in this case, so there is an added TODO.
This commit updates the minimum supported Node.js versions. Node.js v20 support is dropped, and the minimum version for Node.js v22 is bumped to v22.22.0, and for v24 it is bumped to v24.13.1.
BREAKING CHANGE: Node.js v20 is no longer supported. The minimum supported Node.js versions are now v22.22.0 and v24.13.1.
`provideRoutes` was deprecated in v15.
BREAKING CHANGE: `provideRoutes()` has been removed. Use `provideRouter()` or `ROUTES` as multi token if necessary.
In many JS runtimes all closures created in the same scope share a context
this means that data held in one of the closures is not collected until all of the closures are collected.
This change prevents the returned promise from holding a reaction that holds the entire `Router` object in memory.
Enables specifying a custom browser URL for router links via a new input,
allowing navigation to use an explicit browser URL in navigation options.
Closes#66805
Deeply nested parentheses in URLs (e.g. `(a/(b/(c...)))`) trigger recursive calls in `UrlParser`, which can lead to a `RangeError: Maximum call stack size exceeded`. While such errors are generally caught by the framework, relying on the runtime's stack limit is unpredictable across different environments and engine states (e.g. varying stack sizes in different browsers or Node.js versions).
The deeply nested parentheses can cause a stack overflow. While such URLs can be valid (e.g., `(a/(b/(c...)))`) and serialize to simple paths (e.g., /a/b/c), excessive nesting is unreasonable and likely malicious or accidental.
Linear paths (e.g. /a/b/c/d) are parsed iteratively and do NOT trigger recursion. Only parentheses trigger recursion.
This commit introduces a recursion depth limit of 50. If parsing exceeds this depth, the router will now throw a specific `UNPARSABLE_URL` error with the message "URL is too deep". This ensures a deterministic failure mode that is easier for applications to handle than a crash or generic RangeError.
The limit of 50 is chosen as it should accommodate any reasonable application URL structure (including complex named outlets) while providing a safe upper bound against abusive payloads.
This is essentially a refactor of the error state:
* Before: RangeError (System says "I'm out of stack memory")
* After: RuntimeError (Validator says "Input is invalid")
This provides:
* Semantic Correctness: The error now correctly blames the input ("URL too deep"), not the environment ("Stack full").
* Cross-Platform Consistency: The limit is the same in Chrome, Firefox, Node, and Deno, regardless of their internal recursion limits.
* Fast Failure: We stop at depth 50 instead of depth ~15,000, saving those cycles (though CPU cost is negligible either way).
"wide" URLs are now theoretically more expensive than "deep" URLs (because deep ones fail fast), but both are well within safe bounds for any reasonable input size.
This updates `RouterLinkActive`, `Router.isActive`, and the standalone
`isActive` function to accept `Partial<IsActiveMatchOptions>` which uses
the current default values as the base (paths and queryParams are
subset, fragment and matrix params are ignored).
fixes#53326
This commit prevents the Router from intercepting reload navigations
in the navigate event listener. This would convert hard page reloads
to SPA navigations.
fixes#66746
Adds dedicated `LocationStrategy` subclasses: `NoTrailingSlashPathLocationStrategy` and `TrailingSlashPathLocationStrategy`.
The `TrailingSlashPathLocationStrategy` ensures that URLs prepared for the browser always end with a slash, while `NoTrailingSlashPathLocationStrategy` ensures they never do. This configuration only affects the URL written to the browser history; the `Location` service continues to normalize paths by stripping trailing slashes when reading from the browser.
Example:
```typescript
providers: [
{provide: LocationStrategy, useClass: TrailingSlashPathLocationStrategy}
]
```
This approach to the trailing slash problem isolates the changes to the
existing LocationStrategy abstraction without changes to Router, as was
attempted in two other options (#66452 and #66423).
From an architectural perspective, this is the cleanest approach for several reasons:
1. Separation of Concerns and "Router Purity": The Router's primary job is to map a URL structure to an application state (ActivatedRoutes). It shouldn't necessarily be burdened with the formatting nuances of the underlying platform unless those nuances affect the state itself. By pushing trailing slash handling to the LocationStrategy, you treat the trailing slash as a "platform serialization format" rather than a "router state" concern. This avoids the "weirdness" in #66423 where the UrlTree (serialization format) disagrees with the ActivatedRouteSnapshot (logical state).
2. Tree Shakability: If an application doesn't care about trailing slashes (which is the default "never" behavior), they don't pay the cost for that logic. It essentially becomes a swappable "driver" for the URL interaction.
3. Simplicity for the Router: #66452 (consuming the slash as a segment) bleeds into the matching logic, potentially causing issues with child routes or wildcards effectively "eating" a segment that should be invisible. This option leaves the matching logic purely focused on meaningful path segments by continuing to strip the trailing slash on read.
4. Consistency with Existing Patterns: Angular already uses LocationStrategy to handle Hash vs Path routing. Adding "Trailing Slash" nuances there is a natural extension of that pattern—it's just another variation of "how do we represent this logic in the browser's address bar?"
fixes#16051
This builds off of #66197 by retaining the original navigateEvent across redirects
so the NavigateEvent can more accurately track the lifecycle of a navigation,
which may span across several NavigationStart events due to redirects
The `precommitHandler` of the Navigation API unlocks some of the truly
powerful features for Routers like Angular's which defer the URL
updates. Without the `precommitHandler`, we cannot initiate a navigation
until we are ready to commit the URL because it causes the URL to update
immediately.
With `precommitHandler` support, we are able to create a `NavigateEvent`
_immediately_ on navigation, which allows the browser to show that a
navigation is happening with a loading indicator. Site visitors will
also have the ability to cancel the navigation with the "stop" button.
When we are ready to commit the URL, the precommitHandler supports a
"redirect" function that we can use to first redirect the navigation to
a new location immediately before committing it.
The commit operation is not synchronous because the API waits for all
precommitHandlers to resolve. This commit adds a small bit of handling
to account for this so that the Router's transition does not advance
to the next stage until the URL has been committed.