Skip to content

Commit f20b5b5

Browse files
feat(devtools): clean up router tree for stable release
Addresses some cleanup items for the router tree: - No longer loads router ng global APIs as a side effect of importing the router. Rather this is now a runtime step that occurs when provideRouter is called. - No longer depends on router.navigateByUrl in Angular DevTools. There is now a dedicated global util for this - Router instance logic no longer depends on token name - Prevents navigating to lazy or redirect routes (these don't have an associated component)
1 parent 1a26fd3 commit f20b5b5

17 files changed

+317
-165
lines changed

devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ const getRoutesCallback = (messageBus: MessageBus<Events>) => () => getRoutes(me
191191
const navigateRouteCallback = (messageBus: MessageBus<Events>) => (path: string) => {
192192
const router: any = getRouterInstance();
193193
// If the router is not found or the navigateByUrl method is not available, we can't navigate
194-
if (router && router.navigateByUrl) {
195-
router.navigateByUrl(path);
194+
if (router) {
195+
ngDebugClient().ɵnavigateByUrl?.(router, path);
196196
} else {
197197
console.warn('Router not found or navigateByUrl method not available');
198198
}
@@ -207,6 +207,10 @@ const navigateRouteCallback = (messageBus: MessageBus<Events>) => (path: string)
207207
export const viewSourceFromRouter = (name: string, type: RoutePropertyType) => {
208208
const router: any = getRouterInstance();
209209

210+
if (router == null) {
211+
return;
212+
}
213+
210214
let element;
211215
if (type === 'component') {
212216
element = getComponentRefByName(router.config, name);
@@ -477,26 +481,20 @@ export interface SerializableComponentTreeNode
477481
}
478482

479483
function getRouterInstance() {
480-
const forest = prepareForestForSerialization(
481-
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
482-
ngDebugDependencyInjectionApiIsSupported(),
483-
);
484-
const rootInjector = forest[0].resolutionPath?.find((i) => i.name === 'Root');
485-
if (!rootInjector) {
486-
return;
487-
}
484+
const forest = initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest();
485+
const rootNode = forest[0];
488486

489-
const serializedProviderRecords = getSerializedProviderRecords(rootInjector);
490-
const routerInstance = serializedProviderRecords?.find(
491-
(provider) => provider.token === 'Router', // get the instance of router using token
492-
);
487+
if (!rootNode || !rootNode.nativeElement) {
488+
return null;
489+
}
493490

494-
if (!routerInstance) {
495-
return;
491+
const injector = getInjectorFromElementNode(rootNode.nativeElement);
492+
if (!injector) {
493+
return null;
496494
}
497495

498-
const router = getInjectorInstance(rootInjector, routerInstance);
499-
return router;
496+
const ng = ngDebugClient();
497+
return (ng as any).ɵgetRouterInstance?.(injector);
500498
}
501499

502500
// Here we drop properties to prepare the tree for serialization.

devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,10 @@ export function ngDebugProfilerApiIsSupported(): boolean {
7070
/** Checks whether Router API is supported within window.ng */
7171
export function ngDebugRoutesApiIsSupported(): boolean {
7272
const ng = ngDebugClient();
73-
74-
// Temporary solution. Convert to `ɵgetLoadedRoutes` when available.
75-
// If there is a Wiz application, make Routes API unavailable.
76-
const roots = getAppRoots();
7773
return (
78-
!!roots.length &&
79-
!roots.some((el) => {
80-
const comp = ng.getComponent!(el)!;
81-
return ng.getDirectiveMetadata?.(comp)?.framework === Framework.Wiz;
82-
})
74+
ngDebugApiIsSupported(ng, 'ɵgetLoadedRoutes') &&
75+
ngDebugApiIsSupported(ng, 'ɵgetRouterInstance') &&
76+
ngDebugApiIsSupported(ng, 'ɵnavigateByUrl')
8377
);
8478
}
8579

devtools/projects/ng-devtools-backend/src/lib/router-tree.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('parseRoutes', () => {
2020
isAux: false,
2121
isLazy: false,
2222
isActive: false,
23+
isRedirect: false,
2324
});
2425
});
2526

@@ -39,6 +40,7 @@ describe('parseRoutes', () => {
3940
'isAux': false,
4041
'isLazy': false,
4142
'isActive': false,
43+
isRedirect: false,
4244
});
4345
});
4446

@@ -111,6 +113,7 @@ describe('parseRoutes', () => {
111113
'isAux': true,
112114
'isLazy': false,
113115
'isActive': undefined,
116+
'isRedirect': false,
114117
},
115118
{
116119
'component': 'component-two',
@@ -126,6 +129,7 @@ describe('parseRoutes', () => {
126129
'isAux': false,
127130
'isLazy': false,
128131
'isActive': undefined,
132+
'isRedirect': false,
129133
'children': [
130134
{
131135
'component': 'component-two-two',
@@ -141,6 +145,7 @@ describe('parseRoutes', () => {
141145
'isAux': false,
142146
'isLazy': false,
143147
'isActive': undefined,
148+
'isRedirect': false,
144149
},
145150
],
146151
},
@@ -158,6 +163,7 @@ describe('parseRoutes', () => {
158163
'isAux': false,
159164
'isLazy': true,
160165
'isActive': undefined,
166+
'isRedirect': false,
161167
},
162168
{
163169
'component': 'redirect -> redirecting to -> "redirectTo"',
@@ -173,10 +179,12 @@ describe('parseRoutes', () => {
173179
'isAux': false,
174180
'isLazy': false,
175181
'isActive': undefined,
182+
'isRedirect': true,
176183
},
177184
],
178185
'isAux': false,
179186
'isLazy': false,
187+
'isRedirect': false,
180188
'data': [],
181189
'isActive': false,
182190
} as any);

devtools/projects/ng-devtools-backend/src/lib/router-tree.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function parseRoutes(router: Router): Route {
2727
children: rootChildren ? assignChildrenToParent(null, rootChildren, currentUrl) : [],
2828
isAux: false,
2929
isLazy: false,
30+
isRedirect: false,
3031
data: [],
3132
isActive: currentUrl === '/',
3233
};
@@ -62,6 +63,7 @@ function assignChildrenToParent(
6263

6364
// only found in aux routes, otherwise property will be undefined
6465
const isAux = Boolean(child.outlet);
66+
const isRedirect = Boolean(child.redirectTo);
6567
const isLazy = Boolean(child.loadChildren || child.loadComponent);
6668

6769
const pathWithoutParams = routePath.split('/:')[0];
@@ -81,6 +83,7 @@ function assignChildrenToParent(
8183
isAux,
8284
isLazy,
8385
isActive,
86+
isRedirect,
8487
};
8588

8689
if (childDescendents) {

devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
[hidden]="!routerTreeVisible"
8181
[snapToRoot]="snapToRoot()"
8282
[routes]="routes()"
83+
[routerDebugApiSupport]="supportedApis().routes"
8384
/>
8485
}
8586

devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export class DevToolsTabsComponent {
104104
if (supportedApis.dependencyInjection) {
105105
tabs.push('Injector Tree');
106106
}
107-
if (supportedApis.routes && this.routerGraphEnabled() && this.routes().length > 0) {
107+
if (this.routerGraphEnabled() && this.routes().length > 0) {
108108
tabs.push('Router Tree');
109109
}
110110
if (supportedApis.transferState && this.transferStateEnabled()) {

devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.html

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
ng-button
77
size="compact"
88
(click)="btnClick.emit('')"
9-
[disabled]="isRouteWithoutComponent()"
9+
[disabled]="rowValue()?.toString().startsWith('Lazy ')"
1010
>
11-
{{ data() }}
11+
{{ rowValue() }}
1212
</button>
1313
}
1414
@case ('flag') {
15-
<span [class]="data() ? 'tag-active' : 'tag-inactive'">
16-
{{ data() }}
15+
<span [class]="rowValue() ? 'tag-active' : 'tag-inactive'">
16+
{{ rowValue() }}
1717
</span>
1818
}
1919
@case ('list') {
@@ -26,7 +26,13 @@
2626
</div>
2727
}
2828
@default {
29-
<span class="row-data">{{ data() }}</span>
29+
<span class="row-data">
30+
@if (renderValueAsJson()) {
31+
{{ rowValue() | json }}
32+
} @else {
33+
{{ rowValue() }}
34+
}
35+
</span>
3036
}
3137
}
3238
</td>

devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.spec.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ describe('RouteDetailsRowComponent', () => {
2424
component = fixture.componentInstance;
2525

2626
fixture.componentRef.setInput('label', 'Route Title');
27+
fixture.componentRef.setInput('data', {value: 'Route Data'});
28+
fixture.componentRef.setInput('dataKey', 'value');
2729
fixture.detectChanges();
2830
});
2931

@@ -32,8 +34,8 @@ describe('RouteDetailsRowComponent', () => {
3234
});
3335

3436
it('should render a label and text data', () => {
35-
fixture.componentRef.setInput('label', 'Route Title');
36-
fixture.componentRef.setInput('data', 'Route Data');
37+
fixture.componentRef.setInput('data', {value: 'Route Data'});
38+
fixture.componentRef.setInput('dataKey', 'value');
3739
fixture.detectChanges();
3840

3941
const labelElement = fixture.debugElement.query(By.css('th'));
@@ -45,9 +47,9 @@ describe('RouteDetailsRowComponent', () => {
4547
});
4648

4749
it('should render a label and flag data true', () => {
48-
fixture.componentRef.setInput('label', 'Route Title');
4950
fixture.componentRef.setInput('type', 'flag');
50-
fixture.componentRef.setInput('data', 'true');
51+
fixture.componentRef.setInput('data', {isActive: true});
52+
fixture.componentRef.setInput('dataKey', 'isActive');
5153
fixture.detectChanges();
5254

5355
const labelElement = fixture.debugElement.query(By.css('th'));
@@ -59,9 +61,9 @@ describe('RouteDetailsRowComponent', () => {
5961
});
6062

6163
it('should render a label and flag data false', () => {
62-
fixture.componentRef.setInput('label', 'Route Title');
6364
fixture.componentRef.setInput('type', 'flag');
64-
fixture.componentRef.setInput('data', false);
65+
fixture.componentRef.setInput('data', {isLazy: false, isRedirect: false});
66+
fixture.componentRef.setInput('dataKey', 'isLazy');
6567
fixture.detectChanges();
6668

6769
const labelElement = fixture.debugElement.query(By.css('th'));
@@ -73,9 +75,9 @@ describe('RouteDetailsRowComponent', () => {
7375
});
7476

7577
it('should render a label and chip data', () => {
76-
fixture.componentRef.setInput('label', 'Route Title');
7778
fixture.componentRef.setInput('type', 'chip');
78-
fixture.componentRef.setInput('data', 'Component Name');
79+
fixture.componentRef.setInput('data', {name: 'Component Name'});
80+
fixture.componentRef.setInput('dataKey', 'name');
7981
fixture.detectChanges();
8082

8183
const labelElement = fixture.debugElement.query(By.css('th'));
@@ -87,9 +89,9 @@ describe('RouteDetailsRowComponent', () => {
8789
});
8890

8991
it('should render a label and chip data disabled', () => {
90-
fixture.componentRef.setInput('label', 'Route Title');
9192
fixture.componentRef.setInput('type', 'chip');
92-
fixture.componentRef.setInput('data', 'Lazy Component Name');
93+
fixture.componentRef.setInput('data', {name: 'Lazy Component Name'});
94+
fixture.componentRef.setInput('dataKey', 'name');
9395
fixture.detectChanges();
9496

9597
const labelElement = fixture.debugElement.query(By.css('th'));
@@ -102,9 +104,9 @@ describe('RouteDetailsRowComponent', () => {
102104
});
103105

104106
it('should render a label and list data', () => {
105-
fixture.componentRef.setInput('label', 'Route Title');
106107
fixture.componentRef.setInput('type', 'list');
107-
fixture.componentRef.setInput('data', ['Guard 1', 'Guard 2']);
108+
fixture.componentRef.setInput('data', {providers: ['Guard 1', 'Guard 2']});
109+
fixture.componentRef.setInput('dataKey', 'providers');
108110
fixture.detectChanges();
109111

110112
const labelElement = fixture.debugElement.query(By.css('th'));

devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,37 @@
88

99
import {Component, computed, input, output, ChangeDetectionStrategy} from '@angular/core';
1010
import {ButtonComponent} from '../../shared/button/button.component';
11+
import {JsonPipe} from '@angular/common';
12+
import {RouterTreeNode} from './router-tree-fns';
1113

1214
export type RowType = 'text' | 'chip' | 'flag' | 'list';
1315

1416
@Component({
17+
standalone: true,
1518
selector: '[ng-route-details-row]',
1619
templateUrl: './route-details-row.component.html',
1720
styleUrls: ['./route-details-row.component.scss'],
18-
imports: [ButtonComponent],
21+
imports: [ButtonComponent, JsonPipe],
1922
changeDetection: ChangeDetectionStrategy.OnPush,
2023
})
2124
export class RouteDetailsRowComponent {
2225
readonly label = input.required<string>();
23-
readonly data = input<string | boolean | string[]>();
26+
readonly data = input.required<RouterTreeNode>();
27+
readonly dataKey = input.required<string>();
28+
readonly renderValueAsJson = input<boolean>(false);
2429
readonly type = input<RowType>('text');
2530

2631
readonly btnClick = output<string>();
2732

28-
readonly dataArray = computed(() => {
29-
return this.data() as string[];
33+
readonly rowValue = computed(() => {
34+
return this.data()[this.dataKey() as keyof RouterTreeNode];
3035
});
3136

32-
// Lazy and redirecting routes do not have a component associated with them.
33-
// We need to disable the button click event for these routes.
34-
readonly isRouteWithoutComponent = computed(
35-
() =>
36-
this.data()?.toString().includes('Lazy') || this.data()?.toString().includes('redirecting'),
37-
);
37+
readonly dataArray = computed(() => {
38+
if (!this.data() || !this.dataKey()) {
39+
return [];
40+
}
41+
42+
return this.rowValue();
43+
});
3844
}

0 commit comments

Comments
 (0)