Files
Andrew Scott 17d10f7a99 fix(router): set default paramsInheritanceStrategy to 'always'
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.
2026-04-17 14:27:43 -07:00

254 lines
7.9 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {Component} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {ActivatedRouteSnapshot, provideRouter, Router, withRouterConfig} from '../../index';
import {RouterTestingHarness} from '../../testing';
import {EMPTY, interval, NEVER, of} from 'rxjs';
import {useAutoTick} from '@angular/private/testing';
describe('resolveData operator', () => {
useAutoTick();
it('should take only the first emitted value of every resolver', async () => {
TestBed.configureTestingModule({
providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => interval()}}])],
});
await RouterTestingHarness.create('/');
expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({e1: 0});
});
it('should cancel navigation if a resolver does not complete', async () => {
TestBed.configureTestingModule({
providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => EMPTY}}])],
});
await RouterTestingHarness.create('/a');
expect(TestBed.inject(Router).url).toEqual('/');
});
it('should cancel navigation if 1 of 2 resolvers does not emit', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([{path: '**', children: [], resolve: {e0: () => of(0), e1: () => EMPTY}}]),
],
});
await RouterTestingHarness.create('/a');
expect(TestBed.inject(Router).url).toEqual('/');
});
it("should complete instantly if at least one resolver doesn't emit", async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([{path: '**', children: [], resolve: {e0: () => EMPTY, e1: () => NEVER}}]),
],
});
await RouterTestingHarness.create('/a');
expect(TestBed.inject(Router).url).toEqual('/');
});
it('should run resolvers in different parts of the tree', async () => {
let value = 0;
let bValue = 0;
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: 'a',
runGuardsAndResolvers: () => false,
children: [
{
path: '',
resolve: {d0: () => ++value},
runGuardsAndResolvers: 'always',
children: [],
},
],
},
{
path: 'b',
outlet: 'aux',
runGuardsAndResolvers: () => false,
children: [
{
path: '',
resolve: {d1: () => ++bValue},
runGuardsAndResolvers: 'always',
children: [],
},
],
},
]),
],
});
const router = TestBed.inject(Router);
const harness = await RouterTestingHarness.create('/a(aux:b)');
expect(router.routerState.root.children[0]?.firstChild?.snapshot.data).toEqual({d0: 1});
expect(router.routerState.root.children[1]?.firstChild?.snapshot.data).toEqual({d1: 1});
await harness.navigateByUrl('/a(aux:b)#new');
expect(router.routerState.root.children[0]?.firstChild?.snapshot.data).toEqual({d0: 2});
expect(router.routerState.root.children[1]?.firstChild?.snapshot.data).toEqual({d1: 2});
});
it('should update children inherited data when resolvers run', async () => {
let value = 0;
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: 'a',
children: [{path: 'b', children: []}],
resolve: {d0: () => ++value},
runGuardsAndResolvers: 'always',
},
]),
],
});
const harness = await RouterTestingHarness.create('/a/b');
expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({d0: 1});
expect(TestBed.inject(Router).routerState.root.firstChild?.firstChild?.snapshot.data).toEqual({
d0: 1,
});
await harness.navigateByUrl('/a/b#new');
expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({d0: 2});
expect(TestBed.inject(Router).routerState.root.firstChild?.firstChild?.snapshot.data).toEqual({
d0: 2,
});
});
it('should have correct data when parent resolver runs but data is not inherited', async () => {
@Component({
template: '',
standalone: false,
})
class Empty {}
TestBed.configureTestingModule({
providers: [
provideRouter(
[
{
path: 'a',
component: Empty,
data: {parent: 'parent'},
resolve: {other: () => 'other'},
children: [
{
path: 'b',
data: {child: 'child'},
component: Empty,
},
],
},
],
withRouterConfig({paramsInheritanceStrategy: 'emptyOnly'}),
),
],
});
await RouterTestingHarness.create('/a/b');
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
expect(rootSnapshot.data).toEqual({parent: 'parent', other: 'other'});
expect(rootSnapshot.firstChild!.data).toEqual({child: 'child'});
});
it('should have static title when there is a resolver', async () => {
@Component({
template: '',
standalone: false,
})
class Empty {}
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: 'a',
title: 'a title',
component: Empty,
resolve: {other: () => 'other'},
children: [
{
path: 'b',
title: 'b title',
component: Empty,
resolve: {otherb: () => 'other b'},
},
],
},
]),
],
});
await RouterTestingHarness.create('/a/b');
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
expect(rootSnapshot.title).toBe('a title');
expect(rootSnapshot.firstChild!.title).toBe('b title');
});
it('can used parent data in child resolver', async () => {
@Component({
template: '',
})
class Empty {}
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: 'a',
resolve: {
aResolve: () => new Promise<string>((resolve) => setTimeout(() => resolve('a'), 5)),
},
children: [
{
path: 'b',
resolve: {
bResolve: (route: ActivatedRouteSnapshot) => route.data['aResolve'] + 'b',
},
children: [{path: 'c', component: Empty}],
},
],
},
]),
],
});
await RouterTestingHarness.create('/a/b/c');
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
expect(rootSnapshot.firstChild!.firstChild!.data).toEqual({bResolve: 'ab', aResolve: 'a'});
});
it('should inherit resolved data from parent of parent route', async () => {
@Component({
template: '',
standalone: false,
})
class Empty {}
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: 'a',
resolve: {aResolve: () => 'a'},
children: [
{
path: 'b',
resolve: {bResolve: () => 'b'},
children: [{path: 'c', component: Empty}],
},
],
},
]),
],
});
await RouterTestingHarness.create('/a/b/c');
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
expect(rootSnapshot.firstChild!.firstChild!.data).toEqual({bResolve: 'b', aResolve: 'a'});
});
});