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.
This commit introduces a highly requested `trailingSlash` configuration option to the Angular Router, allowing developers to control how trailing slashes are handled in their applications. The options are:
- 'always': Enforces a trailing slash on all URLs.
- 'never': Removes trailing slashes from all URLs (default).
- 'preserve': Respects the presence or absence of a trailing slash as defined in the UrlTree.
This publishes the work that was done to integrate with the Navigation
API as an experimental router feature. Browser support is limited and in
active development. There are also known bugs in the browser implementations
and only Chromium browsers supported deferred URL updates with the
`precommitHandler`. Relates to #53321, which I would likely not mark as
completed until this is at least in dev preview, which likely won't
happen until it is widely available and potentially delayed until
`precommitHandler` is widely available as well.
The final form of this api might not even be a "router feature" in the end, but instead be
something similar to what other frameworks have to provide different
platform integrations (e.g. `provideNavigationRouter`). That would
support omitting the history-based integration from the bundle when only
the navigation integration is used. Alternatively, the current
`provideRouter` could require one of `withHistory` or `withPlatformNavigation`.
Firefox-specific workaround: Firefox 144+ creates the `updateCallbackDone` promise
internally during error handling (e.g., duplicate view-transition-name), even before
the property is accessed. This can cause unhandled promise rejections to appear in
error tracking tools like Rollbar/Sentry. We must attach catch handlers to all three
promises immediately to prevent these unhandled rejections.
See: https://bugzilla.mozilla.org/show_bug.cgi?id=1999336 (if applicable)
Spec: https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-updatecallbackdone
There was a bug introduced in #60875. While RouterLink doesn't
necessarily depend on Router state, its link can change when navigations
cause the `ActivatedRoute` paths to change. It is difficult to determine
which segments the link depends on. Luckily, the commit had flawed
logic:
```
!this.queryParamsHandling && !dependsOnRouterState(this.options?.defaultQueryParamsHandling);
```
The subscription gets created whenever `queryParamsHandling` was not
defined, or it was non-default. This is pretty much all scenarios since
nobody is likely to set it explicitly to the default value. In addition,
the `click` handler recomputes the tree because `urlTree` is a getter
that does the computation.
This commit effectively rolls back #60875