Skip to content

Commit f464e39

Browse files
atscottthePunderWoman
authored andcommitted
fix(router): Ensure title observable gets latest values (#51561)
The data `Observable` is not updated unless there have been changes to the object. The current diffing does not look at `symbol` keys of the object but the `title` property is stored as a private `symbol`. This commit updates the object diffing to include symbols. fixes #51401 PR Close #51561
1 parent e4d2018 commit f464e39

File tree

5 files changed

+46
-15
lines changed

5 files changed

+46
-15
lines changed

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,9 @@
12651265
{
12661266
"name": "getData"
12671267
},
1268+
{
1269+
"name": "getDataKeys"
1270+
},
12681271
{
12691272
"name": "getDeclarationTNode"
12701273
},

packages/router/src/operators/resolve_data.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {ResolveData, Route} from '../models';
1414
import {NavigationTransition} from '../navigation_transition';
1515
import {ActivatedRouteSnapshot, inheritedParamsDataResolve, RouterStateSnapshot} from '../router_state';
1616
import {RouteTitleKey} from '../shared';
17-
import {wrapIntoObservable} from '../utils/collection';
17+
import {getDataKeys, wrapIntoObservable} from '../utils/collection';
1818
import {getClosestRouteInjector} from '../utils/config';
1919
import {getTokenOrFunctionIdentity} from '../utils/preactivation';
2020
import {isEmptyError} from '../utils/type_guards';
@@ -79,10 +79,6 @@ function resolveNode(
7979
);
8080
}
8181

82-
function getDataKeys(obj: Object): Array<string|symbol> {
83-
return [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
84-
}
85-
8682
function getResolver(
8783
injectionToken: ProviderToken<any>|Function, futureARS: ActivatedRouteSnapshot,
8884
futureRSS: RouterStateSnapshot, injector: EnvironmentInjector): Observable<any> {

packages/router/src/utils/collection.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
import {ɵisPromise as isPromise} from '@angular/core';
1010
import {from, isObservable, Observable, of} from 'rxjs';
1111

12-
import {Params} from '../shared';
13-
1412
export function shallowEqualArrays(a: any[], b: any[]): boolean {
1513
if (a.length !== b.length) return false;
1614
for (let i = 0; i < a.length; ++i) {
@@ -19,15 +17,16 @@ export function shallowEqualArrays(a: any[], b: any[]): boolean {
1917
return true;
2018
}
2119

22-
export function shallowEqual(a: Params, b: Params): boolean {
20+
export function shallowEqual(
21+
a: {[key: string|symbol]: any}, b: {[key: string|symbol]: any}): boolean {
2322
// While `undefined` should never be possible, it would sometimes be the case in IE 11
2423
// and pre-chromium Edge. The check below accounts for this edge case.
25-
const k1 = a ? Object.keys(a) : undefined;
26-
const k2 = b ? Object.keys(b) : undefined;
24+
const k1 = a ? getDataKeys(a) : undefined;
25+
const k2 = b ? getDataKeys(b) : undefined;
2726
if (!k1 || !k2 || k1.length != k2.length) {
2827
return false;
2928
}
30-
let key: string;
29+
let key: string|symbol;
3130
for (let i = 0; i < k1.length; i++) {
3231
key = k1[i];
3332
if (!equalArraysOrString(a[key], b[key])) {
@@ -37,6 +36,13 @@ export function shallowEqual(a: Params, b: Params): boolean {
3736
return true;
3837
}
3938

39+
/**
40+
* Gets the keys of an object, including `symbol` keys.
41+
*/
42+
export function getDataKeys(obj: Object): Array<string|symbol> {
43+
return [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
44+
}
45+
4046
/**
4147
* Test equality for arrays of strings or a string.
4248
*/

packages/router/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ts_library(
2323
"//packages/common",
2424
"//packages/common/testing",
2525
"//packages/core",
26+
"//packages/core/rxjs-interop",
2627
"//packages/core/testing",
2728
"//packages/platform-browser",
2829
"//packages/platform-browser-dynamic",

packages/router/test/page_title_strategy_spec.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
import {DOCUMENT} from '@angular/common';
1010
import {provideLocationMocks} from '@angular/common/testing';
11-
import {Component, Inject, Injectable, NgModule} from '@angular/core';
11+
import {Component, inject, Inject, Injectable, NgModule} from '@angular/core';
12+
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
1213
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
13-
import {Router, RouterModule, RouterStateSnapshot, TitleStrategy, withRouterConfig} from '@angular/router';
14-
15-
import {provideRouter} from '../src/provide_router';
14+
import {ActivatedRoute, provideRouter, ResolveFn, Router, RouterModule, RouterStateSnapshot, TitleStrategy, withRouterConfig} from '@angular/router';
15+
import {RouterTestingHarness} from '@angular/router/testing';
1616

1717
describe('title strategy', () => {
1818
describe('DefaultTitleStrategy', () => {
@@ -114,6 +114,31 @@ describe('title strategy', () => {
114114
await router.navigateByUrl('home');
115115
expect(router.routerState.snapshot.root.firstChild!.title).toEqual('My Application');
116116
});
117+
118+
it('pushes updates through the title observable', async () => {
119+
@Component({template: '', standalone: true})
120+
class HomeCmp {
121+
private readonly title$ = inject(ActivatedRoute).title.pipe(takeUntilDestroyed());
122+
title?: string;
123+
124+
constructor() {
125+
this.title$.subscribe(v => this.title = v);
126+
}
127+
}
128+
const titleResolver: ResolveFn<string> = route => route.queryParams.id;
129+
router.resetConfig([{
130+
path: 'home',
131+
title: titleResolver,
132+
component: HomeCmp,
133+
runGuardsAndResolvers: 'paramsOrQueryParamsChange'
134+
}]);
135+
136+
const harness = await RouterTestingHarness.create();
137+
const homeCmp = await harness.navigateByUrl('/home?id=1', HomeCmp);
138+
expect(homeCmp.title).toEqual('1');
139+
await harness.navigateByUrl('home?id=2');
140+
expect(homeCmp.title).toEqual('2');
141+
});
117142
});
118143

119144
describe('custom strategies', () => {

0 commit comments

Comments
 (0)