Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions devtools/projects/ng-devtools-backend/src/lib/router-tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,65 @@ describe('parseRoutes', () => {
expect(parsedRoutes.children![0].canDeactivateGuards).toEqual(['CanDeactivateGuard']);
});

it('should handle matcher function', () => {
function customMatcher() {
return null;
}

const nestedRouter = {
config: [
{
matcher: customMatcher,
component: {name: 'MatcherComponent'},
},
],
};

const parsedRoutes = parseRoutes(nestedRouter as any);
expect(parsedRoutes.children![0].matcher).toEqual('customMatcher()');
expect(parsedRoutes.children![0].path).toEqual('[Matcher]');
});

it('should handle runGuardsAndResolvers with string values', () => {
const nestedRouter = {
config: [
{
path: 'always',
component: {name: 'Component'},
runGuardsAndResolvers: 'always',
},
{
path: 'params',
component: {name: 'Component2'},
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
},
],
};

const parsedRoutes = parseRoutes(nestedRouter as any);
expect(parsedRoutes.children![0].runGuardsAndResolvers).toEqual('always');
expect(parsedRoutes.children![1].runGuardsAndResolvers).toEqual('paramsOrQueryParamsChange');
});

it('should handle runGuardsAndResolvers with function', () => {
function customRerunLogic() {
return true;
}

const nestedRouter = {
config: [
{
path: 'custom',
component: {name: 'Component'},
runGuardsAndResolvers: customRerunLogic,
},
],
};

const parsedRoutes = parseRoutes(nestedRouter as any);
expect(parsedRoutes.children![0].runGuardsAndResolvers).toEqual('customRerunLogic()');
});

it('should handle resolvers with named functions', () => {
function userResolver() {
return {id: 1, name: 'User'};
Expand Down
47 changes: 32 additions & 15 deletions devtools/projects/ng-devtools-backend/src/lib/router-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export type RoutePropertyType =
| 'component'
| 'redirectTo'
| 'title'
| 'resolvers';
| 'resolvers'
| 'matcher'
| 'runGuardsAndResolvers';

export type RouteGuard = 'canActivate' | 'canActivateChild' | 'canDeactivate' | 'canMatch';

Expand Down Expand Up @@ -149,6 +151,17 @@ function assignChildrenToParent(
routeConfig.redirectTo = getPropertyName(child, 'redirectTo');
}

if (child.matcher) {
routeConfig.matcher = getPropertyName(child, 'matcher');
// For custom matchers, override the path to indicate it's a matcher
// Since the path be undefined when using a matcher, because the matcher defines the path matching
routeConfig.path = '[Matcher]';
}

if (child.runGuardsAndResolvers) {
routeConfig.runGuardsAndResolvers = getPropertyName(child, 'runGuardsAndResolvers');
}

if (childDescendents) {
routeConfig.children = assignChildrenToParent(
routeConfig.path,
Expand Down Expand Up @@ -211,7 +224,10 @@ function getClassOrFunctionName(fn: Function, defaultName?: string) {
return isClass ? fn.name : `${fn.name}()`;
}

function getPropertyName(child: AngularRoute, property: 'title' | 'redirectTo') {
function getPropertyName(
child: AngularRoute,
property: 'title' | 'redirectTo' | 'matcher' | 'runGuardsAndResolvers',
) {
if (child[property] instanceof Function) {
return getClassOrFunctionName(child[property], property);
}
Expand Down Expand Up @@ -242,14 +258,6 @@ export function getElementRefByName(
name: string,
): any | null {
for (const element of routes) {
if (type === 'title' && element.title instanceof Function) {
const functionName = getClassOrFunctionName(element.title);
//TODO: improve this, not every titleFn has a name property
if (functionName === name) {
return element.title;
}
}

if (type === 'resolvers' && element.resolve) {
for (const key in element.resolve) {
if (element.resolve.hasOwnProperty(key)) {
Expand All @@ -262,11 +270,20 @@ export function getElementRefByName(
}
}

if (type === 'redirectTo' && element.redirectTo instanceof Function) {
const functionName = getClassOrFunctionName(element.redirectTo);
//TODO: improve this, not every redirectToFn has a name property
if (functionName === name) {
return element.redirectTo;
const functionProperties: Exclude<RoutePropertyType, 'resolvers'>[] = [
'title',
'redirectTo',
'matcher',
'runGuardsAndResolvers',
];

for (const property of functionProperties) {
if (type === property && element[property] instanceof Function) {
const functionName = getClassOrFunctionName(element[property]);
// TODO: improve this, not every function has a name property
if (functionName === name) {
return element[property];
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,27 @@ <h2 class="router-title">Routes Details</h2>
<!-- TODO: Convert to a description list (<dl>) -->
<div class="scrollable-wrapper">
<table class="ng-table">
<tr
ng-route-details-row
label="Path"
dataKey="path"
[data]="data"
actionBtnType="navigate"
[actionBtnTooltip]="'Navigate to ' + data.path"
(actionBtnClick)="navigateRoute(route)"
></tr>
@if (!data.matcher) {
<tr
ng-route-details-row
label="Path"
dataKey="path"
[data]="data"
actionBtnType="navigate"
[actionBtnTooltip]="'Navigate to ' + data.path"
(actionBtnClick)="navigateRoute(route)"
></tr>
} @else {
<tr
ng-route-details-row
label="Matcher"
dataKey="matcher"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewFunctionSource(data.matcher, 'matcher')"
></tr>
}

@if (!data.redirectTo) {
<tr
Expand Down Expand Up @@ -176,6 +188,19 @@ <h2 class="router-title">Routes Details</h2>
(actionBtnClick)="viewFunctionSource(data.title, 'title')"
></tr>
}

@if (data.runGuardsAndResolvers) {
<tr
ng-route-details-row
label="RunGuardsAndResolvers"
dataKey="runGuardsAndResolvers"
[data]="data"
actionBtnTooltip="View source"
[actionBtnType]="hasStaticOptionRunGuardsAndResolvers() ? 'none' : 'view-source'"
(actionBtnClick)="viewFunctionSource(data.runGuardsAndResolvers, 'runGuardsAndResolvers')"
></tr>
}

<tr
ng-route-details-row
label="Active"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar';
import {ApplicationOperations} from '../../application-operations/index';
import {RouteDetailsRowComponent} from './route-details-row.component';
import {FrameManager} from '../../application-services/frame_manager';
import {Events, MessageBus, Route} from '../../../../../protocol';
import {Events, MessageBus, Route, RunGuardsAndResolvers} from '../../../../../protocol';
import {SvgD3Node, TreeVisualizerConfig} from '../../shared/tree-visualizer/tree-visualizer';
import {
RouterTreeD3Node,
Expand All @@ -39,6 +39,13 @@ import {SplitAreaDirective} from '../../shared/split/splitArea.directive';
import {Debouncer} from '../../shared/utils/debouncer';

const SEARCH_DEBOUNCE = 250;
const RUN_GUARDS_AND_RESOLVERS_OPTIONS: RunGuardsAndResolvers[] = [
'pathParamsChange',
'pathParamsOrQueryParamsChange',
'always',
'paramsChange',
'paramsOrQueryParamsChange',
];

@Component({
selector: 'ng-router-tree',
Expand Down Expand Up @@ -69,8 +76,14 @@ export class RouterTreeComponent {
return this.selectedRoute()?.data;
});

readonly routes = input.required<Route[]>();
readonly routerDebugApiSupport = input(false);
protected hasStaticOptionRunGuardsAndResolvers = computed(() =>
RUN_GUARDS_AND_RESOLVERS_OPTIONS.includes(
this.routeData()?.runGuardsAndResolvers as RunGuardsAndResolvers,
),
);

routes = input.required<Route[]>();
routerDebugApiSupport = input<boolean>(false);

private readonly showFullPath = signal(false);
protected readonly d3RootNode = linkedSignal<RouterTreeNode | null>(() => {
Expand Down Expand Up @@ -150,7 +163,10 @@ export class RouterTreeComponent {
);
}

viewFunctionSource(functionName: string, type: 'title' | 'redirectTo'): void {
viewFunctionSource(
functionName: string,
type: 'title' | 'redirectTo' | 'matcher' | 'runGuardsAndResolvers',
): void {
if (functionName === '[Function]') {
const message =
'Cannot view the source of redirect functions defined inline (arrow or anonymous).';
Expand Down
13 changes: 12 additions & 1 deletion devtools/projects/protocol/src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,18 @@ export interface Route {
isActive: boolean;
isAux: boolean;
isLazy: boolean;
}
matcher?: string;
runGuardsAndResolvers?:
| 'pathParamsChange'
| 'pathParamsOrQueryParamsChange'
| 'paramsChange'
| 'paramsOrQueryParamsChange'
| 'always'
| (string & {});
}

type OnlyLiterals<T> = T extends string ? (string extends T ? never : T) : never;
export type RunGuardsAndResolvers = OnlyLiterals<Route['runGuardsAndResolvers']>;

export interface AngularDetection {
// This is necessary because the runtime
Expand Down
27 changes: 27 additions & 0 deletions devtools/src/app/demo-app/todo/routes/routes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ActivatedRouteSnapshot,
Resolve,
ResolveFn,
UrlSegment,
} from '@angular/router';

import {
Expand Down Expand Up @@ -42,6 +43,17 @@ export const activateGuard: CanActivateFn = (
return true;
};

export const customMatcher = (url: UrlSegment[]) => {
if (url.length === 1 && url[0].path === 'custom-matcher') {
return {consumed: url};
}
return null;
};

export function customRerunLogic() {
return true;
}

@NgModule({
imports: [
CommonModule,
Expand All @@ -68,6 +80,21 @@ export const activateGuard: CanActivateFn = (
providers: [Service1, Service2, Service3, Service4],
loadComponent: () => import('./routes.component').then((x) => x.RoutesStandaloneComponent),
},
{
matcher: customMatcher,
component: RoutesHomeComponent,
},
{
path: 'route-run-guards-and-resolvers',
component: RoutesHomeComponent,
canActivate: [activateGuard],
runGuardsAndResolvers: 'always',
},
{
path: 'route-run-guards-and-resolvers-function',
component: RoutesHomeComponent,
runGuardsAndResolvers: customRerunLogic,
},
{
path: 'route-data',
component: RoutesHomeComponent,
Expand Down