Skip to content

Commit fbc83a6

Browse files
authored
feat(router): add user-defined detail type to ViewConfig (#3431)
1 parent 79d2a7e commit fbc83a6

File tree

4 files changed

+53
-5
lines changed

4 files changed

+53
-5
lines changed

packages/ts/file-router/src/runtime/createMenuItems.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function hasVariablePathSegment(path: string): boolean {
2323
*
2424
* @returns A list of menu items.
2525
*/
26-
export function createMenuItems(): readonly MenuItem[] {
26+
export function createMenuItems<T = unknown>(): ReadonlyArray<MenuItem<T>> {
2727
// @ts-expect-error: esbuild injection
2828
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
2929
__REGISTER__('createMenuItems', (window as VaadinWindow).Vaadin);
@@ -44,6 +44,7 @@ export function createMenuItems(): readonly MenuItem[] {
4444
icon: config.menu?.icon,
4545
title: config.menu?.title ?? config.title,
4646
order: config.menu?.order,
47+
detail: config.detail as T | undefined,
4748
}))
4849
// Sort views according to the order specified in the view configuration.
4950
.sort((menuA, menuB) => {

packages/ts/file-router/src/shared/internal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import type { RouteParamType } from './routeParamType.js';
55
* Internal type used for server communication and menu building. It extends the
66
* view configuration with the route parameters.
77
*/
8-
export type ServerViewConfig = Readonly<{
8+
export type ServerViewConfig<T = unknown> = Readonly<{
99
children?: readonly ServerViewConfig[];
1010
params?: Readonly<Record<string, RouteParamType>>;
1111
}> &
12-
ViewConfig;
12+
ViewConfig<T>;
1313

1414
export type VaadinObject = Readonly<{
1515
views: Readonly<Record<string, ViewConfig>>;

packages/ts/file-router/src/types.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { createBrowserRouter, RouteObject } from 'react-router';
22

3-
export type ViewConfig = Readonly<{
3+
/**
4+
* A configuration object for a view. This is used to define the view's
5+
* metadata, such as the title, roles allowed, and other properties.
6+
*
7+
* @typeParam T - The type of the detail object.
8+
*/
9+
export type ViewConfig<T = unknown> = Readonly<{
410
/**
511
* View title used in the main layout header, as <title> and as the default
612
* for the menu entry. If not defined, the component name will be taken,
@@ -59,6 +65,14 @@ export type ViewConfig = Readonly<{
5965
*/
6066
icon?: string;
6167
}>;
68+
69+
/**
70+
* Used to add additional properties to the view. This object will be
71+
* available when building the menu.
72+
*
73+
* @see {@link ./runtime/createMenuItems.ts#createMenuItems}
74+
*/
75+
detail?: T;
6276
}>;
6377

6478
/**
@@ -87,11 +101,14 @@ export type AgnosticRoute = Readonly<{
87101

88102
/**
89103
* A menu item used in for building the navigation menu.
104+
*
105+
* @typeParam T - The type of the detail object, same as in the view configuration.
90106
*/
91-
export type MenuItem = Readonly<{
107+
export type MenuItem<T = unknown> = Readonly<{
92108
to: string;
93109
icon?: string;
94110
title?: string;
111+
detail?: T;
95112
}>;
96113

97114
export type RouterConfiguration = Readonly<{

packages/ts/file-router/test/runtime/createMenuItems.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import './vaadinGlobals.js'; // eslint-disable-line import/no-unassigned-import
22
import { expect, describe, it } from 'vitest';
33
import { createMenuItems, viewsSignal } from '../../src/runtime/createMenuItems.js';
4+
import type { ViewConfig } from '../../src/types.js';
45
import { deepRemoveNullProps } from '../utils.js';
56

67
const collator = new Intl.Collator('en-US');
@@ -165,5 +166,34 @@ describe('@vaadin/hilla-file-router', () => {
165166
},
166167
]);
167168
});
169+
170+
it('should include detail objects in menu items', () => {
171+
// used to check that the type of the detail object is correct
172+
// this is a compile-time check, does nothing at runtime
173+
type Detail = {
174+
foo: string;
175+
bar?: number;
176+
};
177+
178+
const views: Record<string, ViewConfig<Detail>> = {
179+
'/bar': { title: 'Bar', detail: { foo: '1', bar: 2 } },
180+
'/foo': { title: 'Foo', detail: { foo: '3' } },
181+
};
182+
viewsSignal.value = views;
183+
184+
const items = createMenuItems<Detail>();
185+
expect(deepRemoveNullProps(items)).to.be.deep.equal([
186+
{
187+
title: 'Bar',
188+
to: '/bar',
189+
detail: { foo: '1', bar: 2 },
190+
},
191+
{
192+
title: 'Foo',
193+
to: '/foo',
194+
detail: { foo: '3' },
195+
},
196+
]);
197+
});
168198
});
169199
});

0 commit comments

Comments
 (0)