Skip to content

Commit d36b564

Browse files
committed
feat: add tableRowReplace injection for custom row rendering in list view
1 parent 485c3a9 commit d36b564

File tree

4 files changed

+87
-10
lines changed

4 files changed

+87
-10
lines changed

adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,70 @@ cd custom
396396
npm i @iconify-prerendered/vue-mdi
397397
```
398398
399+
## List table row replace injection
400+
401+
`tableRowReplace` lets you fully control how each list table row is rendered. Instead of the default table `<tr></tr>` markup, AdminForth will mount your Vue component per record and use its returned DOM to display the row. Use this when you need custom row layouts, extra controls, or conditional styling that goes beyond column-level customization.
402+
403+
Supported forms:
404+
- Single component: `pageInjections.list.tableRowReplace = '@@/MyRowRenderer.vue'`
405+
- Object form with meta: `pageInjections.list.tableRowReplace = { file: '@@/MyRowRenderer.vue', meta: { /* optional */ } }`
406+
- If an array is provided, the first element is used.
407+
408+
Example configuration:
409+
410+
```ts title="/resources/apartments.ts"
411+
{
412+
resourceId: 'aparts',
413+
...
414+
options: {
415+
pageInjections: {
416+
list: {
417+
tableRowReplace: {
418+
file: '@@/ApartRowRenderer.vue',
419+
meta: {
420+
// You can pass any meta your component may read
421+
}
422+
}
423+
}
424+
}
425+
}
426+
}
427+
```
428+
429+
Minimal component example (decorate default row with a border):
430+
431+
```vue title="/custom/ApartRowRenderer.vue"
432+
<template>
433+
<tr class="border border-gray-200 dark:border-gray-700 rounded-sm">
434+
<slot />
435+
</tr>
436+
437+
</template>
438+
439+
<script setup lang="ts">
440+
import { computed } from 'vue';
441+
const props = defineProps<{
442+
record: any
443+
resource: any
444+
meta: any
445+
}>();
446+
</script>
447+
```
448+
449+
Component contract:
450+
- Inputs
451+
- `record`: the current record object
452+
- `resource`: the resource config object
453+
- `meta`: the meta object passed in the injection config
454+
- Slots
455+
- Default slot: the table’s standard row content (cells) will be projected here. Your component can wrap or style it.
456+
- Output
457+
- Render a full `<tr></tr>` fragment. For simple decoration, render a single `<td :colspan="columnsCount">` and wrap `<slot />` inside your layout.
458+
459+
Notes and tips:
460+
- Requirements:
461+
- Required <tr></tr> structure around </slot>
462+
399463
## List table beforeActionButtons
400464
401465
`beforeActionButtons` allows injecting one or more compact components into the header bar of the list page, directly to the left of the default action buttons (`Create`, `Filter`, bulk actions, three‑dots menu). Use it for small inputs (quick search, toggle, status chip) rather than large panels.

adminforth/modules/configValidator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@ export default class ConfigValidator implements IConfigValidator {
869869
// Validate page-specific allowed injection keys
870870
const possiblePages = ['list', 'show', 'create', 'edit'];
871871
const allowedInjectionsByPage: Record<string, string[]> = {
872-
list: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'beforeActionButtons', 'bottom', 'threeDotsDropdownItems', 'customActionIcons', 'tableBodyStart'],
872+
list: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'beforeActionButtons', 'bottom', 'threeDotsDropdownItems', 'customActionIcons', 'tableBodyStart', 'tableRowReplace'],
873873
show: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'],
874874
edit: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'saveButton'],
875875
create: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'saveButton'],

adminforth/spa/src/components/ResourceListTableVirtual.vue

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,21 @@
9393
</tr>
9494

9595
<!-- Visible rows -->
96-
<tr @click="onClick($event,row)"
97-
v-for="(row, rowI) in visibleRows"
98-
:key="`row_${row._primaryKeyValue}`"
99-
ref="rowRefs"
100-
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
101-
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
102-
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
103-
>
96+
<component
97+
v-else
98+
v-for="(row, rowI) in visibleRows"
99+
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
100+
:key="`row_${row._primaryKeyValue}`"
101+
:record="row"
102+
:resource="resource"
103+
:adminUser="coreStore.adminUser"
104+
:meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
105+
@click="onClick($event, row)"
106+
ref="rowRefs"
107+
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
108+
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
109+
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
110+
>
104111
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading" @click="(e)=>e.stopPropagation()">
105112
<Checkbox
106113
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
@@ -224,7 +231,7 @@
224231
</template>
225232
</div>
226233
</td>
227-
</tr>
234+
</component>
228235

229236
<!-- Bottom spacer -->
230237
<tr v-if="totalHeight > 0">

adminforth/spa/src/views/ListView.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@
143143
? [coreStore.resourceOptions.pageInjections.list.tableBodyStart]
144144
: []
145145
"
146+
:tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace)
147+
? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0]
148+
: coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null"
146149
:container-height="1100"
147150
:item-height="52.5"
148151
:buffer-size="listBufferSize"
@@ -173,6 +176,9 @@
173176
? [coreStore.resourceOptions.pageInjections.list.tableBodyStart]
174177
: []
175178
"
179+
:tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace)
180+
? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0]
181+
: coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null"
176182
/>
177183

178184
<component

0 commit comments

Comments
 (0)