Skip to content

Commit 68c7bee

Browse files
authored
Merge pull request #419 from devforth/feature/tableRowReplace-injection
feat: add tableRowReplace injection for custom row rendering in list …
2 parents fd7ab4e + efe8f41 commit 68c7bee

File tree

6 files changed

+121
-20
lines changed

6 files changed

+121
-20
lines changed

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,79 @@ 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+
adminUser: any
446+
}>();
447+
</script>
448+
```
449+
450+
Component contract:
451+
- Inputs
452+
- `record`: the current record object
453+
- `resource`: the resource config object
454+
- `meta`: the meta object passed in the injection config
455+
- Slots
456+
- Default slot: the table’s standard row content (cells) will be projected here. Your component can wrap or style it.
457+
- Output
458+
- Render a full `<tr></tr>` fragment. For example, to replace the standard set of cells with a single full‑width cell, render:
459+
460+
```vue
461+
<tr>
462+
<td :colspan="columnsCount">
463+
<slot />
464+
</td>
465+
</tr>
466+
```
467+
468+
Notes and tips:
469+
- Requirements:
470+
- Required `<tr></tr>` structure around `<slot />`
471+
399472
## List table beforeActionButtons
400473
401474
`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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export default class ConfigValidator implements IConfigValidator {
4545
}
4646

4747
validateAndListifyInjection(obj, key, errors) {
48+
if (key.includes('tableRowReplace')) {
49+
if (obj[key].length > 1) {
50+
throw new Error(`tableRowReplace injection supports only one element, but received ${obj[key].length}.`);
51+
}
52+
}
4853
if (!Array.isArray(obj[key])) {
4954
// not array
5055
obj[key] = [obj[key]];
@@ -869,7 +874,7 @@ export default class ConfigValidator implements IConfigValidator {
869874
// Validate page-specific allowed injection keys
870875
const possiblePages = ['list', 'show', 'create', 'edit'];
871876
const allowedInjectionsByPage: Record<string, string[]> = {
872-
list: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'beforeActionButtons', 'bottom', 'threeDotsDropdownItems', 'customActionIcons', 'tableBodyStart'],
877+
list: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'beforeActionButtons', 'bottom', 'threeDotsDropdownItems', 'customActionIcons', 'tableBodyStart', 'tableRowReplace'],
873878
show: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'],
874879
edit: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'saveButton'],
875880
create: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'saveButton'],

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,20 @@
8383
</td>
8484
</tr>
8585

86-
<tr @click="onClick($event,row)"
87-
v-else v-for="(row, rowI) in rows" :key="`row_${row._primaryKeyValue}`"
88-
ref="rowRefs"
89-
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
90-
91-
:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
92-
>
86+
<component
87+
v-else
88+
v-for="(row, rowI) in rows"
89+
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
90+
:key="`row_${row._primaryKeyValue}`"
91+
:record="row"
92+
:resource="resource"
93+
:adminUser="coreStore.adminUser"
94+
:meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
95+
@click="onClick($event, row)"
96+
ref="rowRefs"
97+
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
98+
:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
99+
>
93100
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
94101
<Checkbox
95102
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
@@ -210,7 +217,7 @@
210217
</div>
211218

212219
</td>
213-
</tr>
220+
</component>
214221
</tbody>
215222
</table>
216223
</div>
@@ -328,7 +335,7 @@ import {
328335
} from '@iconify-prerendered/vue-flowbite';
329336
import router from '@/router';
330337
import { Tooltip } from '@/afcl';
331-
import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon } from '@/types/Common';
338+
import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
332339
import adminforth from '@/adminforth';
333340
import Checkbox from '@/afcl/Checkbox.vue';
334341
@@ -345,6 +352,7 @@ const props = defineProps<{
345352
noRoundings?: boolean,
346353
customActionsInjection?: any[],
347354
tableBodyStartInjection?: any[],
355+
tableRowReplaceInjection?: AdminForthComponentDeclaration,
348356
}>();
349357
350358
// emits, update page

adminforth/spa/src/components/ResourceListTableVirtual.vue

Lines changed: 18 additions & 10 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">
@@ -350,7 +357,7 @@ import {
350357
} from '@iconify-prerendered/vue-flowbite';
351358
import router from '@/router';
352359
import { Tooltip } from '@/afcl';
353-
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon } from '@/types/Common';
360+
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
354361
import adminforth from '@/adminforth';
355362
import Checkbox from '@/afcl/Checkbox.vue';
356363
@@ -370,6 +377,7 @@ const props = defineProps<{
370377
containerHeight?: number,
371378
itemHeight?: number,
372379
bufferSize?: number,
380+
tableRowReplaceInjection?: AdminForthComponentDeclaration
373381
}>();
374382
375383
// emits, update page

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 || undefined"
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 || undefined"
176182
/>
177183

178184
<component

adminforth/types/Common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ export interface AdminForthResourceInputCommon {
504504
threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
505505
customActionIcons?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
506506
tableBodyStart?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
507+
tableRowReplace?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
507508
},
508509

509510
/**

0 commit comments

Comments
 (0)