Commit graph

576 commits

Author SHA1 Message Date
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
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
0960592d3d fix(router): pass outlet context to split to fix empty path named outlets
The `split` helper function in `packages/router/src/utils/config_matching.ts` was blind to the current outlet being processed. When encountering an empty path named outlet in the config, it would assume it needed to pull it in as a synthetic empty group, even if we were already in the process of resolving that very outlet!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

fixes #67708

(cherry picked from commit daa9b2a9d6)
2026-04-01 11:48:47 +02:00
Andrew Scott
84adb2fb3b refactor(router): Permit deferring commit of traversal navigations
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.

(cherry picked from commit 778b748694)
2026-03-03 22:09:15 +00:00
Angular Robot
5b8a403220 build: update rules_browsers digest to ceb5275
See associated pull request for more information.

Closes #67141 as a pr takeover
2026-02-19 16:01:02 -08:00
SkyZeroZx
0f47fda51b test(router): move timeout and autoTick helpers to shared testing utilities
Centralizes common test helpers under testing utilities and updates usages
2026-02-10 07:45:00 -08:00
Angular Robot
11767cabe4 build: update Jasmine to 6.0.0
Jasmine enables `forbidDuplicateNames: true` by default. So we also need to desambiguate duplicate spec names.
2026-02-09 12:15:57 -08:00
Jessica Janiuk
5a0f272519 Revert "feat(router): adds browserUrl input support to router links"
This reverts commit 9505541d32.
2026-02-02 16:32:09 -08:00
SkyZeroZx
68ba9c45cb test(router): remove provider zoneless from tests
Removes the `provideZonelessChangeDetection` provider from router tests.
It’s no longer needed and simplifies the test setup.
2026-02-02 15:00:18 -08:00
SkyZeroZx
9505541d32 feat(router): adds browserUrl input support to router links
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
2026-02-02 11:08:18 -08:00
Andrew Scott
458bc4a2c8 fix(router): limit UrlParser recursion depth to prevent stack overflow
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.
2026-01-29 12:15:21 -08:00
Andrew Scott
907a94dcec feat(router): Update IsActiveMatchOptions APIs to accept a Partial
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
2026-01-29 12:10:40 -08:00
Andrew Scott
cf9620f7d0 feat(router): Make match options optional in isActive
The behavior now matches RouterLinkActive.
2026-01-29 12:10:40 -08:00
Andrew Scott
b51bab583d feat(router): Add partial ActivatedRouteSnapshot information to canMatch params
This commit adds partial `ActivatedRouteSnapshot` information as the
third parameter of the `canMatch` guard.

resolves #49309
2026-01-26 23:36:06 +00:00
Andrew Scott
dbd50be7f7 fix(router): Do not intercept reload events with Navigation integration
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
2026-01-26 22:29:51 +00:00
Andrew Scott
8bbe6dc46c feat(common): Add Location strategies to manage trailing slash on write
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
2026-01-23 20:09:23 +00:00
Andrew Scott
e8c44a00f9 refactor(router): Retain original navigateEvent across redirects
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
2026-01-12 14:41:54 -08:00
Andrew Scott
da364d2635 refactor(router): Add support for precommitHandler in Navigation integration
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.
2026-01-09 10:31:26 -08:00
Jessica Janiuk
a2b9429992 Revert "feat(router): add trailingSlash config option"
This reverts commit 12fccc5e99.
2026-01-08 12:20:03 -08:00
Andrew Scott
12fccc5e99
feat(router): add trailingSlash config option
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.
2026-01-08 08:26:37 -08:00
Andrew Scott
7003e8d241 feat(router): Publish Router's integration with platform Navigation API as experimental
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`.
2026-01-07 16:16:06 -08:00
Andrew Scott
bcef77d950 fix(router): Fix RouterLink href not updating with queryParamsHandling
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
2026-01-07 14:08:13 -05:00
Andreas Dorner
1c00ab42f8 feat(router): extend paramters of RedirectFunction to include paramMap and queryParamMap
adds paramMap and queryParamMap to the partial ActivatedRoute of the
RedirectFn

fixes ##60842
2026-01-07 12:36:54 -05:00
Andrew Scott
97fd1de0ac Revert "refactor(router): Add support for precommitHandler in Navigation integration"
This reverts commit 522fa716b8.
2026-01-07 11:45:58 -05:00
Andrew Scott
397dbc4c37 Revert "refactor(router): Retain original navigateEvent across redirects"
This reverts commit 53d3ae0feb.
2026-01-07 11:45:58 -05:00
Andrew Scott
174d2da29a Revert "fix(router): Ensure createUrlTree does not reuse segments of input"
This reverts commit 39efb62c0f.
2026-01-06 16:29:19 -08:00
Andrew Scott
53d3ae0feb refactor(router): Retain original navigateEvent across redirects
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
2026-01-06 16:10:56 -05:00
Andrew Scott
522fa716b8 refactor(router): Add support for precommitHandler in Navigation integration
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.
2026-01-06 16:10:56 -05:00
Andrew Scott
39efb62c0f fix(router): Ensure createUrlTree does not reuse segments of input
Mutating the `UrlTree` that is returned by `createUrlTree` can cause the
input, which might even be an active route, to be mutated. This ensures
the `UrlSegment`s are recreated and do not mutate the input.

fixes #54624
2026-01-06 13:11:54 -05:00
Andrew Scott
5edceffd04
feat(router): add controls for route cleanup
This commit introduces a new feature to automatically destroy `EnvironmentInjector`s associated with routes that are no longer active or stored. This helps in managing memory by releasing resources held by unused injectors.
2026-01-05 14:43:56 -05:00
Matthieu Riegler
6270bba056 ci: reformat files
This is after we've slightly changed a rule in #66056
2025-12-16 14:44:19 -08:00
Andrew Scott
a0ad5d4b2b test(router): Add tests for support of path params before/after a wildcard
This adds additional tests following the initial implementation in #64737 that cover
the use-case of path parameters before and after the wildcard segment.
2025-12-02 16:46:17 +01:00
Andrew Scott
cc03254b12 refactor(router): Avoid aborting traversal navigations
Unnecessarily aborting traversal navigations will break focus and scroll
restoration. We need to keep them open rather than replacing them with
an identical router-initiated navigation.
2025-12-02 16:45:17 +01:00
Andrew Scott
1ab461c846 refactor(router): Store route injector on ActivatedRoute instance
This eliminates the need to pass around the EnvironmentInjector
everywhere we need the injection context for a route.
2025-11-20 17:05:10 -05:00
Andrew Scott
c25d749d85 feat(router): Execute RunGuardsAndResolvers function in injection context
Allows more sophisticated checks based on information available in DI
(e.g. the router state). Use-cases have been described in #53944 / https://github.com/angular/angular/issues/31843#issuecomment-1890955590

resolves #53944
2025-11-20 17:05:10 -05:00
Andrew Scott
2db8f2ee75 refactor(router): Add handling of abort event on signal of NavigateEvent
This adds handling of the abort event on the signal of the
`NavigateEvent`, allowing us to cancel the Router's ongoing navigation
transition when its related navigation was aborted.
2025-11-10 08:02:43 -08:00
Andrew Scott
1651908004
refactor(router): Add handler to NavigationInterceptOptions
This adds a handler to the `NavigationInterceptOptions` when we are
intercepting a `NavigateEvent`. This means that the scroll and focus
restoration will be delayed until the handler promise resolves. It also
means that we can provide better indication of an ongoing navigation
event.
2025-11-07 13:32:49 -08:00
Andrew Scott
85d9abb019
refactor(router): Intercept navigate events (unless it's a rollback)
This adds further integration with the browser Navigation API by intercepting the navigation events.
2025-11-06 15:00:08 -08:00
SkyZeroZx
08a84e03e6 refactor(router): simplify imports and improve option access in RouterScroller tests
Simplifies imports and unifies option access in RouterScroller specs for cleaner and more consistent code
2025-11-06 09:42:41 -08:00
Andrew Scott
189807ef04 refactor(router): Build out integration with browser Navigation API (#64905)
This further builds out the Router integration with the platform
Navigation API. Key features in this state include:

* History restoration via direct platform APIs rather than markers left on
  `history.state`. This means more guaranteed correctness and less
  internal code to compute traversal restorations.
* Ability to observe navigations triggered outside the Router APIs.
  Practically speaking, this means some navigations can be performed
  through the platform rather than requiring `Router.navigate`. Note
  that because the `NavigateEvent` is never intercepted at this point of
  the implementation, regular anchor tags cannot be used because they
  will still trigger a popstate navigation.

This implementation does _not_ intercept the `NavigateEvent` but future
iterations should. By omitting the interception, we are missing out on
features such as:

* Platform-supported scroll and focus reset
* Holding the navigate event open for the duration of the router
  navigation, allowing for a visual loading indicator in the browser
* Support for intercepting navigations from regular anchor tags (e.g.
  not `RouterLink`s) and converting those to SPA navigations.

PR Close #64905
2025-11-06 17:42:04 +00:00
Andrew Scott
7f16902a72 refactor(router): Update pre/post wildcard matching to retain 0 or more segment match meaning
Prior to the change to support defining required segments before and after the wildcard, the meaning of the wildcard
path was '0 or more' so path: foo with a child of ** will still match the ** route when the URL is only 'foo'.
If developers want to _require_ something, they can still use path: foo/:anyRequired/**/bar
2025-11-04 17:56:54 +00:00
Andrew Scott
c84d372778 feat(router): Support wildcard params with segments trailing (#64737)
this adds support for both leading and trailing segments before/after wildcard
route. Exposig the segments in a new _splat param would require a
breaking change to the return value of the matchers.

fixes https://github.com/angular/angular/issues/60821

PR Close #64737
2025-10-30 15:44:16 +00:00
Andrew Scott
5a93eeb6c0 Revert "feat(router): Support wildcard params with segments trailing"
This reverts commit 0a0cc27aea.
Causes internal failures with route matching. Should investigate and add
tests to prevent future regressions.
2025-10-28 18:35:27 +01:00
Andrew Scott
0a0cc27aea feat(router): Support wildcard params with segments trailing
this adds support for both leading and trailing segments before/after wildcard
route. Exposig the segments in a new _splat param would require a
breaking change to the return value of the matchers.

fixes https://github.com/angular/angular/issues/60821
2025-10-28 10:23:58 +01:00
Andrew Scott
a03c82564d feat(router): Add scroll behavior controls on router navigation
This commit adds the ability to control the behavior of scrolling in the
`NavigationBehaviorOptions`. The options directly correlate with the
intercept options of the Navigation API `NavigateEvent#intercept`:
https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/intercept#scroll

While we do not currently have an integration with the navigation API,
this change would be necessary to provide the ability to configure that behavior
if/when we do. In the meantime, this option is also useful to control
the behavior of scrolling when the in memory scroller is enabled.

resolves #26744
2025-10-27 09:24:26 +01:00
Andrew Scott
dd09da8ba2
refactor(router): Add provider for integrating with Navigation API and Location shim
This adds a (private) provider for integrating with the browser Navigation API.
This provider ensures that interactions with the `Location` service
use the underlying platform navigation rather than the history and
location APIs.
2025-10-27 09:21:56 +01:00
Andrew Scott
e2346dbfac refactor(router): Compress middle of navigation pipeline to fewer operators (#64480)
This cleans up the navigation transitions a bit by removing some
unnecessary operators. Combining operators makes debugging easier by
making it possible to step through the code.

PR Close #64480
2025-10-24 09:31:05 +02:00
Andrew Scott
d4d6c28023 fix(router): handle parenthesized outlets without a name in DefaultUrlSerializer (#64507)
Previously, the `DefaultUrlSerializer` would incorrectly parse URLs with a parenthesized outlet that did not have a name, such as `/(left)`. This would result in an `undefined` outlet name in the serialized URL.

This commit fixes the issue by ensuring that parenthesized outlets without a name are treated as primary outlets.

fixes #58516. Based on the description, either the URL was constructed
manually or by custom serializer.

PR Close #64507
2025-10-23 12:33:57 +02:00
Kristiyan Kostadinov
9d48e534e2 Revert "feat(router): allow router outlet to be set on ng-container (#64562)" (#64584)
This reverts commit 2bd764a3c4.

PR Close #64584
2025-10-22 16:35:38 +00:00
Kristiyan Kostadinov
2bd764a3c4 feat(router): allow router outlet to be set on ng-container (#64562)
Updates the selector for `RouterOutlet` to allow for it to be set on an `ng-container`. This allows it to not render the host node which can affect the layout.

Fixes #64553.

PR Close #64562
2025-10-21 16:49:06 +00:00