From fb5c8bb2f59e1ea34605a93573a6dad5f9aaef13 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Thu, 7 May 2026 18:23:41 +0200 Subject: [PATCH 01/22] feat(ContentSearch): add `search` function prop for external search providers --- docs/app/app.vue | 12 +-- docs/app/components/search/Search.vue | 27 +++--- docs/app/error.vue | 7 +- docs/package.json | 2 +- pnpm-lock.yaml | 89 ++++++++++++++++++- src/runtime/components/CommandPalette.vue | 10 ++- .../components/content/ContentSearch.vue | 77 +++++++++++----- src/runtime/composables/useContentSearch.ts | 60 ++++++++++++- src/runtime/locale/ar.ts | 1 + src/runtime/locale/az.ts | 1 + src/runtime/locale/be.ts | 1 + src/runtime/locale/bg.ts | 1 + src/runtime/locale/bn.ts | 1 + src/runtime/locale/ca.ts | 1 + src/runtime/locale/ckb.ts | 1 + src/runtime/locale/cs.ts | 1 + src/runtime/locale/da.ts | 1 + src/runtime/locale/de.ts | 1 + src/runtime/locale/de_ch.ts | 1 + src/runtime/locale/el.ts | 1 + src/runtime/locale/en.ts | 1 + src/runtime/locale/es.ts | 1 + src/runtime/locale/et.ts | 1 + src/runtime/locale/eu.ts | 1 + src/runtime/locale/fa_ir.ts | 1 + src/runtime/locale/fi.ts | 1 + src/runtime/locale/fr.ts | 1 + src/runtime/locale/gl.ts | 1 + src/runtime/locale/he.ts | 1 + src/runtime/locale/hi.ts | 1 + src/runtime/locale/hr.ts | 1 + src/runtime/locale/hu.ts | 1 + src/runtime/locale/hy.ts | 1 + src/runtime/locale/id.ts | 1 + src/runtime/locale/is.ts | 1 + src/runtime/locale/it.ts | 1 + src/runtime/locale/ja.ts | 1 + src/runtime/locale/ka.ts | 1 + src/runtime/locale/kk.ts | 1 + src/runtime/locale/km.ts | 1 + src/runtime/locale/ko.ts | 1 + src/runtime/locale/ky.ts | 1 + src/runtime/locale/lb.ts | 1 + src/runtime/locale/lo.ts | 1 + src/runtime/locale/lt.ts | 1 + src/runtime/locale/mn.ts | 1 + src/runtime/locale/ms.ts | 1 + src/runtime/locale/nb_no.ts | 1 + src/runtime/locale/nl.ts | 1 + src/runtime/locale/pl.ts | 1 + src/runtime/locale/pt.ts | 1 + src/runtime/locale/pt_br.ts | 1 + src/runtime/locale/ro.ts | 1 + src/runtime/locale/ru.ts | 1 + src/runtime/locale/sk.ts | 1 + src/runtime/locale/sl.ts | 1 + src/runtime/locale/sq.ts | 1 + src/runtime/locale/sv.ts | 1 + src/runtime/locale/th.ts | 1 + src/runtime/locale/tj.ts | 1 + src/runtime/locale/tr.ts | 1 + src/runtime/locale/ug_cn.ts | 1 + src/runtime/locale/uk.ts | 1 + src/runtime/locale/ur.ts | 1 + src/runtime/locale/uz.ts | 1 + src/runtime/locale/vi.ts | 1 + src/runtime/locale/zh_cn.ts | 1 + src/runtime/locale/zh_tw.ts | 1 + src/runtime/types/locale.ts | 1 + src/runtime/utils/{fuse.ts => search.ts} | 13 +++ src/theme/command-palette.ts | 6 +- 71 files changed, 300 insertions(+), 64 deletions(-) rename src/runtime/utils/{fuse.ts => search.ts} (89%) diff --git a/docs/app/app.vue b/docs/app/app.vue index 0187e833c2..ceb008cf43 100644 --- a/docs/app/app.vue +++ b/docs/app/app.vue @@ -7,11 +7,6 @@ const colorMode = useColorMode() const { style, link } = useTheme() const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs', ['framework', 'category', 'description'])) -const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs', { - ignoredTags: ['style'] -}), { - server: false -}) const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white') @@ -20,10 +15,7 @@ useHead({ { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { key: 'theme-color', name: 'theme-color', content: color } ], - link: computed(() => [ - // { rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }, - ...link.value - ]), + link, style, htmlAttrs: { lang: 'en' @@ -75,7 +67,7 @@ provide('navigation', rootNavigation) - + diff --git a/docs/app/components/search/Search.vue b/docs/app/components/search/Search.vue index 64df767fa5..0bd0ca3c59 100644 --- a/docs/app/components/search/Search.vue +++ b/docs/app/components/search/Search.vue @@ -1,25 +1,24 @@ + + +``` + +::note +When using the `search` prop, you don't need to pass `files` — the component will use the async search function on each keystroke instead of Fuse.js. Results are automatically mapped and grouped by navigation. +:: + ## API ### Props diff --git a/src/runtime/components/content/ContentSearch.vue b/src/runtime/components/content/ContentSearch.vue index 9105d7f2cd..9478864787 100644 --- a/src/runtime/components/content/ContentSearch.vue +++ b/src/runtime/components/content/ContentSearch.vue @@ -57,12 +57,6 @@ export interface ContentSearchProps Promise<(ContentSearchFile & { snippets?: { title?: string, content?: string } })[]> /** * Options for [useFuse](https://vueuse.org/integrations/useFuse) passed to the [CommandPalette](https://ui.nuxt.com/docs/components/command-palette). * @defaultValue { @@ -78,8 +72,19 @@ export interface ContentSearchProps /** - * Delay (in milliseconds) before the search term is passed to Fuse (debounced). - * Useful for large doc sets where running fuzzy search on every keystroke is the bottleneck — the input stays responsive while Fuse only re-runs after typing settles. + * Async search function (e.g. from [`useSearchCollection`](https://content.nuxt.com/docs/utils/use-search-collection)). + * When provided, ContentSearch calls it on each keystroke and uses the results instead of Fuse. + * Results are mapped, sanitized, and grouped by navigation internally. + */ + search?: (query: string, opts?: any) => Promise<(ContentSearchFile & { snippets?: { title?: string, content?: string } })[]> + /** + * Status of the async search index (e.g. from `useSearchCollection`). + * When the status transitions to `'ready'`, the search is automatically re-triggered if there's a pending term. + */ + searchStatus?: string + /** + * Delay (in milliseconds) before the search is triggered (debounced). + * Keeps the input responsive by only running the search after typing settles. * Set to `0` to disable. * @defaultValue 100 */ @@ -159,10 +164,8 @@ const debouncedSearchTerm = refDebounced(searchTerm, () => props.searchDelay!) const searchResults = shallowRef([]) const searching = ref(false) -watch(debouncedSearchTerm, async (term) => { - if (!props.search) return - - if (!term) { +async function runSearch(term: string) { + if (!props.search || !term) { searchResults.value = [] return } @@ -178,6 +181,20 @@ watch(debouncedSearchTerm, async (term) => { searchResults.value = [] } searching.value = false +} + +watch(debouncedSearchTerm, runSearch) + +watch(() => props.search, () => { + if (debouncedSearchTerm.value) { + runSearch(debouncedSearchTerm.value) + } +}) + +watch(() => props.searchStatus, (status) => { + if (status === 'ready' && debouncedSearchTerm.value) { + runSearch(debouncedSearchTerm.value) + } }) const linksGroup = computed(() => { diff --git a/test/components/content/ContentSearch.spec.ts b/test/components/content/ContentSearch.spec.ts index 27cac9dd24..c0528f8d96 100644 --- a/test/components/content/ContentSearch.spec.ts +++ b/test/components/content/ContentSearch.spec.ts @@ -129,6 +129,7 @@ describe('ContentSearch', () => { ['with loadingIcon', { props: { ...props, loading: true, loadingIcon: 'i-lucide-loading' } }], ['without colorMode', { props: { ...props, colorMode: false } }], ['with fullscreen', { props: { ...props, fullscreen: true } }], + ['with search function', { props: { ...props, search: async () => [], files: undefined } }], ...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]), ['with ui', { props: { ...props, ui: { input: '[&>input]:text-lg' } } }], ['with class', { props: { ...props, class: 'sm:max-w-5xl' } }] diff --git a/test/components/content/__snapshots__/ContentSearch.spec.ts.snap b/test/components/content/__snapshots__/ContentSearch.spec.ts.snap index 42a89c4a24..0d8950eaa6 100644 --- a/test/components/content/__snapshots__/ContentSearch.spec.ts.snap +++ b/test/components/content/__snapshots__/ContentSearch.spec.ts.snap @@ -431,6 +431,63 @@ exports[`ContentSearch > renders with placeholder correctly 1`] = ` +" +`; + +exports[`ContentSearch > renders with search function correctly 1`] = ` +" + + + + + +
+ + + + + " `; From 254ad3ea4944c53760acd88b7be2a850817380e2 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Sat, 9 May 2026 14:07:48 +0200 Subject: [PATCH 07/22] chore(deps): update content --- docs/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/package.json b/docs/package.json index f146dbcc30..12baa20fd1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,7 +19,7 @@ "@iconify-json/simple-icons": "^1.2.80", "@iconify-json/vscode-icons": "^1.2.48", "@nuxt/a11y": "1.0.0-alpha.1", - "@nuxt/content": "https://pkg.pr.new/@nuxt/content@80dc9c2", + "@nuxt/content": "https://pkg.pr.new/@nuxt/content@dbafed4", "@nuxt/image": "^2.0.0", "@nuxt/ui": "workspace:*", "@nuxtjs/mcp-toolkit": "^0.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2a47bbd68..9c35f88910 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,8 +344,8 @@ importers: specifier: 1.0.0-alpha.1 version: 1.0.0-alpha.1(magicast@0.5.2)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2)) '@nuxt/content': - specifier: https://pkg.pr.new/@nuxt/content@80dc9c2 - version: https://pkg.pr.new/@nuxt/content@80dc9c2(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3)) + specifier: https://pkg.pr.new/@nuxt/content@dbafed4 + version: https://pkg.pr.new/@nuxt/content@dbafed4(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3)) '@nuxt/image': specifier: ^2.0.0 version: 2.0.0(db0@0.3.4(better-sqlite3@12.9.0))(ioredis@5.9.3)(magicast@0.5.2) @@ -1720,8 +1720,8 @@ packages: valibot: optional: true - '@nuxt/content@https://pkg.pr.new/@nuxt/content@80dc9c2': - resolution: {integrity: sha512-GYqcK7N+8BE0ddeS+vbHiv1e5ogM5KmK8Xz1GxAXOZ+sgwRSp2UdeQYKKL3HqCCGGpyFJwtoDyMX2RccdRymTA==, tarball: https://pkg.pr.new/@nuxt/content@80dc9c2} + '@nuxt/content@https://pkg.pr.new/@nuxt/content@dbafed4': + resolution: {integrity: sha512-tsyu8zeWOYtfqhOKi3JicIJIZ7NZ2pndXUpYHfU7UJkiVb/UuMKv37gBDJcOvfaHmFalCLeoqCf03SA4zmQ6BQ==, tarball: https://pkg.pr.new/@nuxt/content@dbafed4} version: 3.13.0 engines: {node: '>= 20.19.0'} peerDependencies: @@ -9829,7 +9829,7 @@ snapshots: - supports-color - utf-8-validate - '@nuxt/content@https://pkg.pr.new/@nuxt/content@80dc9c2(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3))': + '@nuxt/content@https://pkg.pr.new/@nuxt/content@dbafed4(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3))': dependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) '@nuxtjs/mdc': 0.21.1(magicast@0.5.2) From 9ae9205bf70874dffb38f9b04369700ab246a752 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Sat, 9 May 2026 14:07:57 +0200 Subject: [PATCH 08/22] docs: remove unfinished doc --- docs/content/docs/3.composables/use-form-field.md | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 docs/content/docs/3.composables/use-form-field.md diff --git a/docs/content/docs/3.composables/use-form-field.md b/docs/content/docs/3.composables/use-form-field.md deleted file mode 100644 index c4c2893ce7..0000000000 --- a/docs/content/docs/3.composables/use-form-field.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: useFormField -description: 'A composable to integrate custom inputs with the Form component' -navigation: false ---- - -## Usage - -Use the auto-imported `useFormField` composable to integrate custom inputs with a [Form](/docs/components/form). - -```vue - -``` From 6b51aa4412cc63e06cc8e47bbe088205bdf639c7 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Sat, 9 May 2026 14:08:03 +0200 Subject: [PATCH 09/22] chore(Search): clean --- docs/app/components/search/Search.vue | 4 +++- docs/app/composables/useSearch.ts | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/app/components/search/Search.vue b/docs/app/components/search/Search.vue index 3c43d23936..84a29d560d 100644 --- a/docs/app/components/search/Search.vue +++ b/docs/app/components/search/Search.vue @@ -14,6 +14,8 @@ const { links, groups, searchTerm } = useSearch() const { open } = useContentSearch() const { track } = useAnalytics() +const fuse = { resultLimit: 20 } + watch(open, (value) => { if (value && status.value === 'idle') { init() @@ -35,6 +37,6 @@ watchDebounced(searchTerm, (term) => { :navigation="navigation" :search="search" :search-status="status" - :fuse="{ resultLimit: 20 }" + :fuse="fuse" /> diff --git a/docs/app/composables/useSearch.ts b/docs/app/composables/useSearch.ts index 380596c164..70df2fac3d 100644 --- a/docs/app/composables/useSearch.ts +++ b/docs/app/composables/useSearch.ts @@ -104,6 +104,10 @@ export function useSearch() { }]) const groups = computed(() => [{ + id: 'framework', + label: 'Framework', + items: frameworks.value + }, { id: 'ai', label: 'AI', ignoreFilter: true, @@ -122,10 +126,6 @@ export function useSearch() { }, onSelect }] - }, { - id: 'framework', - label: 'Framework', - items: frameworks.value }]) return { From 4562d47e46b7846f8702ff842c42dbe244a2cda7 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Sat, 9 May 2026 14:08:14 +0200 Subject: [PATCH 10/22] fix(useContentSearch): remove filter on navigation --- src/runtime/composables/useContentSearch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/runtime/composables/useContentSearch.ts b/src/runtime/composables/useContentSearch.ts index 7fa4e04191..164ba3f6bd 100644 --- a/src/runtime/composables/useContentSearch.ts +++ b/src/runtime/composables/useContentSearch.ts @@ -112,8 +112,6 @@ function _useContentSearch() { const basePath = result.id.split('#')[0]! const { link, parent, root } = findNavItem(basePath, navigation) - if (navigation?.length && !link) return null - return { label: result.title, labelHtml: result.snippets?.title ? sanitizeSnippet(result.snippets.title) : undefined, From 1b7e2946024e3489709968bd157052adaea8ed25 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Mon, 11 May 2026 09:46:56 +0200 Subject: [PATCH 11/22] docs(form): remove useless link --- docs/content/docs/2.components/form.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/2.components/form.md b/docs/content/docs/2.components/form.md index af3bb3de9e..8d3beb394e 100644 --- a/docs/content/docs/2.components/form.md +++ b/docs/content/docs/2.components/form.md @@ -147,7 +147,7 @@ options: :: ::tip -You can use the [`useFormField`](/docs/composables/use-form-field) composable to implement this inside your own components. +You can use the `useFormField` composable to implement this inside your own components. :: ### Error event From bbbfbecf56b716acb6707ea953279db41e5e3f2e Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Mon, 11 May 2026 09:47:29 +0200 Subject: [PATCH 12/22] fix(CommandPalette): remove useless cast --- src/runtime/components/CommandPalette.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/components/CommandPalette.vue b/src/runtime/components/CommandPalette.vue index 26eed9e72a..f857f29f9f 100644 --- a/src/runtime/components/CommandPalette.vue +++ b/src/runtime/components/CommandPalette.vue @@ -354,7 +354,7 @@ function processGroupItems(group: G, items: (T & { matches?: FuseResult['matc ...item, labelHtml: item.labelHtml ?? highlight(item, fuseSearchTerm.value, props.labelKey!), suffixHtml: item.suffixHtml ?? highlight(item, fuseSearchTerm.value, 'suffix' as GetItemKeys, [props.labelKey!]), - descriptionHtml: (item as any).descriptionHtml ?? highlight(item, fuseSearchTerm.value, props.descriptionKey as GetItemKeys, [props.labelKey!, 'suffix' as GetItemKeys]) + descriptionHtml: item.descriptionHtml ?? highlight(item, fuseSearchTerm.value, props.descriptionKey as GetItemKeys, [props.labelKey!, 'suffix' as GetItemKeys]) } }) } From eff6aed009ca2e05e709bd08379052ed1bea5cb6 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Mon, 11 May 2026 11:34:59 +0200 Subject: [PATCH 13/22] chore(deps): update --- docs/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/package.json b/docs/package.json index 12baa20fd1..28c607a338 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,7 +19,7 @@ "@iconify-json/simple-icons": "^1.2.80", "@iconify-json/vscode-icons": "^1.2.48", "@nuxt/a11y": "1.0.0-alpha.1", - "@nuxt/content": "https://pkg.pr.new/@nuxt/content@dbafed4", + "@nuxt/content": "https://pkg.pr.new/@nuxt/content@606bf64", "@nuxt/image": "^2.0.0", "@nuxt/ui": "workspace:*", "@nuxtjs/mcp-toolkit": "^0.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c35f88910..b70e720ad9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,8 +344,8 @@ importers: specifier: 1.0.0-alpha.1 version: 1.0.0-alpha.1(magicast@0.5.2)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2)) '@nuxt/content': - specifier: https://pkg.pr.new/@nuxt/content@dbafed4 - version: https://pkg.pr.new/@nuxt/content@dbafed4(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3)) + specifier: https://pkg.pr.new/@nuxt/content@606bf64 + version: https://pkg.pr.new/@nuxt/content@606bf64(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3)) '@nuxt/image': specifier: ^2.0.0 version: 2.0.0(db0@0.3.4(better-sqlite3@12.9.0))(ioredis@5.9.3)(magicast@0.5.2) @@ -1720,8 +1720,8 @@ packages: valibot: optional: true - '@nuxt/content@https://pkg.pr.new/@nuxt/content@dbafed4': - resolution: {integrity: sha512-tsyu8zeWOYtfqhOKi3JicIJIZ7NZ2pndXUpYHfU7UJkiVb/UuMKv37gBDJcOvfaHmFalCLeoqCf03SA4zmQ6BQ==, tarball: https://pkg.pr.new/@nuxt/content@dbafed4} + '@nuxt/content@https://pkg.pr.new/@nuxt/content@606bf64': + resolution: {tarball: https://pkg.pr.new/@nuxt/content@606bf64} version: 3.13.0 engines: {node: '>= 20.19.0'} peerDependencies: @@ -9829,7 +9829,7 @@ snapshots: - supports-color - utf-8-validate - '@nuxt/content@https://pkg.pr.new/@nuxt/content@dbafed4(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3))': + '@nuxt/content@https://pkg.pr.new/@nuxt/content@606bf64(better-sqlite3@12.9.0)(magicast@0.5.2)(valibot@1.3.1(typescript@6.0.3))': dependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) '@nuxtjs/mdc': 0.21.1(magicast@0.5.2) From 0a6c3fc4e4fdbe8e5a91708e10068052bb048aa4 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Mon, 11 May 2026 11:40:10 +0200 Subject: [PATCH 14/22] fix(useContentSearch): missing `>` in prefix --- src/runtime/composables/useContentSearch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/composables/useContentSearch.ts b/src/runtime/composables/useContentSearch.ts index 164ba3f6bd..7c507e0ef6 100644 --- a/src/runtime/composables/useContentSearch.ts +++ b/src/runtime/composables/useContentSearch.ts @@ -80,7 +80,7 @@ function _useContentSearch() { children: undefined } as ContentSearchItem, ...(link.children?.map(child => ({ ...child, - prefix: link.label, + prefix: link.label + ' >', suffix: child.description, description: undefined, icon: child.icon || link.icon || appConfig.ui.icons.file From 73e451cf007bb1f6b4ed756d2c5feac96c2cda19 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Mon, 11 May 2026 16:18:09 +0200 Subject: [PATCH 15/22] fix(CommandPalette): omit `descriptionHtml` from emitted value and clean up - Add `descriptionHtml` to the `omit()` list in ListboxItem value binding (consistent with `labelHtml` / `suffixHtml`) - Remove unused `searching` ref in ContentSearch - Fix `mapSearchResult` return type (never returns null) --- src/runtime/components/CommandPalette.vue | 2 +- src/runtime/components/content/ContentSearch.vue | 5 +---- src/runtime/composables/useContentSearch.ts | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/runtime/components/CommandPalette.vue b/src/runtime/components/CommandPalette.vue index f857f29f9f..75a95ede23 100644 --- a/src/runtime/components/CommandPalette.vue +++ b/src/runtime/components/CommandPalette.vue @@ -482,7 +482,7 @@ function onSelect(e: Event, item: T) { & {