Appearance
Migrating from FTTable to FTGrid
Tip: hit Copy to put this whole guide on your clipboard, then paste it into your AI assistant alongside your
FTTablecode to scaffold the migration.
FTGrid is not a drop-in replacement for FTTable. They solve different problems:
FTTableis slot-driven — you template every cell in markup. Great for small, bespoke lists where each row is hand-crafted.FTGridis config-driven — you describe columns with acellType, and the grid renders them. Great when you need AG Grid's column ergonomics: resize, pin, reorder, the column picker, external filters, CSV export, and large server-paged datasets.
Migrate when your table has outgrown hand-templated rows — when you find yourself re-implementing sorting, filtering, column visibility, or server pagination by hand. Stay on FTTable for short, static, slot-heavy lists.
The core shift: slots → columns
This is the one concept that changes everything. In FTTable you own the row markup; in FTGrid you declare what each column is and let the grid render it.
vue
<!-- FTTable: you template the cell -->
<FTTable :heads="heads" :items="items">
<template #row="item">
<tr>
<td>{{ item.name }}</td>
<td><span class="dot" :style="{ background: statusColor(item.status) }" />{{ item.status }}</td>
</tr>
</template>
</FTTable><!-- FTTable: you template the cell -->
<FTTable :heads="heads" :items="items">
<template #row="item">
<tr>
<td>{{ item.name }}</td>
<td><span class="dot" :style="{ background: statusColor(item.status) }" />{{ item.status }}</td>
</tr>
</template>
</FTTable>vue
<!-- FTGrid: you declare the column -->
<FTGrid :columns="columns" :items="items" />
<!--
columns = [
{ field: 'name', headerName: 'Name', cellType: 'text' },
{ field: 'status', headerName: 'Status', cellType: 'status',
statusMap: { Active: '#16a34a', Inactive: '#9ca3af' } },
]
--><!-- FTGrid: you declare the column -->
<FTGrid :columns="columns" :items="items" />
<!--
columns = [
{ field: 'name', headerName: 'Name', cellType: 'text' },
{ field: 'status', headerName: 'Status', cellType: 'status',
statusMap: { Active: '#16a34a', Inactive: '#9ca3af' } },
]
-->Prop mapping
FTTable | FTGrid | Notes |
|---|---|---|
heads: (string | FTTableHead)[] | columns: IFTGridColumnDef[] | Columns now carry cellType + per-type options |
#row / #head slots | columns config (+ cellRendererOverrides) | Cell rendering moves from markup to config |
items | items | Same — array of row objects |
withPaging | always paginated | Pagination is built in; no flag needed |
defaultPageSize | pageSize | Initial rows per page |
defaultSortKey / defaultSortOrder | column sortable + click-to-sort | Client sort is automatic per sortable column |
totalItems | totalItems | Same — server's grand total |
simplePagination | simplePagination | Same — the server-side switch |
page | page | Same — controlled current page |
noStripes / isWrapped | rowStyle ('zebra'/'lines'/'none'…) | Row decoration is a single enum |
Event mapping
The server-side events were intentionally aligned with FTTable so this layer migrates verbatim:
FTTable | FTGrid | Payload |
|---|---|---|
page-changed | page-changed | page: number |
size-changed | size-changed | size: number |
sorting-changed | sorting-changed | { sortKey: string; sortOrder: 'asc' | 'desc' | '' } |
| — | selection-changed | rows[] — new capability via utility: 'checkbox' |
Cell-type recipes
Most #row slot markup maps to a built-in cellType:
| You were rendering… | Use cellType |
|---|---|
| plain text / a number | 'text' / 'number' |
| a formatted date | 'date' (+ grid-level dateFormat) |
money (€1,240.50) | 'currency' (+ currencySymbol) |
| a coloured status dot + label | 'status' (+ statusMap) |
| a coloured pill / tag | 'badge' (+ badgeMap) |
| an avatar image / initials | 'avatar' (+ avatarField) |
| a country flag | 'flag' (+ flagCdnBase) |
| anything custom | cellRendererOverrides — a render fn or a component |
Use cases
1. Simple list
A plain FTTable with three text columns.
vue
<!-- Before -->
<FTTable :heads="['Name', 'Email', 'Country']" :items="users">
<template #row="user">
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.country }}</td>
</tr>
</template>
</FTTable><!-- Before -->
<FTTable :heads="['Name', 'Email', 'Country']" :items="users">
<template #row="user">
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.country }}</td>
</tr>
</template>
</FTTable>vue
<!-- After -->
<script setup lang="ts">
import type { IFTGridColumnDef } from '@fasttrack-solutions/vue-components-lib';
const columns: IFTGridColumnDef[] = [
{ field: 'name', headerName: 'Name', cellType: 'text' },
{ field: 'email', headerName: 'Email', cellType: 'text' },
{ field: 'country', headerName: 'Country', cellType: 'text' },
];
</script>
<template>
<FTGrid :columns="columns" :items="users" :utility="'none'" />
</template><!-- After -->
<script setup lang="ts">
import type { IFTGridColumnDef } from '@fasttrack-solutions/vue-components-lib';
const columns: IFTGridColumnDef[] = [
{ field: 'name', headerName: 'Name', cellType: 'text' },
{ field: 'email', headerName: 'Email', cellType: 'text' },
{ field: 'country', headerName: 'Country', cellType: 'text' },
];
</script>
<template>
<FTGrid :columns="columns" :items="users" :utility="'none'" />
</template>2. Status dots and badges (slot → cellType + map)
The classic case: a #row slot computing colours by value.
vue
<!-- Before -->
<FTTable :heads="['Player', 'Status', 'Tier']" :items="players">
<template #row="p">
<tr>
<td>{{ p.name }}</td>
<td>
<span class="dot" :style="{ background: statusColor(p.status) }" />
{{ p.status }}
</td>
<td><span class="pill" :class="`pill--${p.tier}`">{{ p.tier }}</span></td>
</tr>
</template>
</FTTable><!-- Before -->
<FTTable :heads="['Player', 'Status', 'Tier']" :items="players">
<template #row="p">
<tr>
<td>{{ p.name }}</td>
<td>
<span class="dot" :style="{ background: statusColor(p.status) }" />
{{ p.status }}
</td>
<td><span class="pill" :class="`pill--${p.tier}`">{{ p.tier }}</span></td>
</tr>
</template>
</FTTable>vue
<!-- After: colours become declarative maps -->
const columns: IFTGridColumnDef[] = [ { field: 'name', headerName: 'Player', cellType: 'text',
filterType: 'condition' }, { field: 'status', headerName: 'Status', cellType: 'status', filterType:
'set', statusMap: { Active: '#16a34a', Pending: '#f59e0b', Inactive: '#9ca3af' }, }, { field:
'tier', headerName: 'Tier', cellType: 'badge', badgeMap: { Gold: { bg: '#fef3c7', fg: '#92400e' },
Silver: { bg: '#e5e7eb', fg: '#374151' }, Bronze: { bg: '#fde68a', fg: '#78350f' }, }, }, ];<!-- After: colours become declarative maps -->
const columns: IFTGridColumnDef[] = [ { field: 'name', headerName: 'Player', cellType: 'text',
filterType: 'condition' }, { field: 'status', headerName: 'Status', cellType: 'status', filterType:
'set', statusMap: { Active: '#16a34a', Pending: '#f59e0b', Inactive: '#9ca3af' }, }, { field:
'tier', headerName: 'Tier', cellType: 'badge', badgeMap: { Gold: { bg: '#fef3c7', fg: '#92400e' },
Silver: { bg: '#e5e7eb', fg: '#374151' }, Bronze: { bg: '#fde68a', fg: '#78350f' }, }, }, ];3. Currency, dates, and right-alignment
vue
const columns: IFTGridColumnDef[] = [ { field: 'name', headerName: 'Customer', cellType: 'text' }, {
field: 'balance', headerName: 'Balance', cellType: 'currency', currencySymbol: '€', align: 'right'
}, { field: 'lastSeen', headerName: 'Last Seen', cellType: 'date' }, ];const columns: IFTGridColumnDef[] = [ { field: 'name', headerName: 'Customer', cellType: 'text' }, {
field: 'balance', headerName: 'Balance', cellType: 'currency', currencySymbol: '€', align: 'right'
}, { field: 'lastSeen', headerName: 'Last Seen', cellType: 'date' }, ];vue
<!-- dateFormat applies to every `date` column -->
<FTGrid :columns="columns" :items="rows" dateFormat="relative" /><!-- dateFormat applies to every `date` column -->
<FTGrid :columns="columns" :items="rows" dateFormat="relative" />4. Server-side pagination + sorting (large datasets)
This is the headline reason to migrate. With FTTable you fetched a page and fed it in; FTGrid keeps the exact same controlled model, so the wiring carries over one-to-one.
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { FTGrid } from '@fasttrack-solutions/vue-components-lib';
import type { IFTGridColumnDef } from '@fasttrack-solutions/vue-components-lib';
const columns: IFTGridColumnDef[] = [
{ field: 'name', headerName: 'Player', cellType: 'text', sortable: true },
{
field: 'balance',
headerName: 'Balance',
cellType: 'currency',
currencySymbol: '€',
sortable: true,
},
];
const rows = ref<Record<string, unknown>[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(25);
const sort = ref<{ sortKey: string; sortOrder: 'asc' | 'desc' | '' }>({
sortKey: '',
sortOrder: '',
});
async function fetchPage() {
const res = await api.getPlayers({
page: page.value,
pageSize: pageSize.value,
sortKey: sort.value.sortKey,
sortOrder: sort.value.sortOrder,
});
rows.value = res.data; // just this page
total.value = res.total; // grand total
}
onMounted(fetchPage);
</script>
<template>
<FTGrid
:columns="columns"
:items="rows"
simplePagination
:totalItems="total"
:page="page"
:pageSize="pageSize"
@page-changed="
(p) => {
page = p;
fetchPage();
}
"
@size-changed="
(s) => {
pageSize = s;
page = 1;
fetchPage();
}
"
@sorting-changed="
(s) => {
sort = s;
fetchPage();
}
"
/>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue';
import { FTGrid } from '@fasttrack-solutions/vue-components-lib';
import type { IFTGridColumnDef } from '@fasttrack-solutions/vue-components-lib';
const columns: IFTGridColumnDef[] = [
{ field: 'name', headerName: 'Player', cellType: 'text', sortable: true },
{
field: 'balance',
headerName: 'Balance',
cellType: 'currency',
currencySymbol: '€',
sortable: true,
},
];
const rows = ref<Record<string, unknown>[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(25);
const sort = ref<{ sortKey: string; sortOrder: 'asc' | 'desc' | '' }>({
sortKey: '',
sortOrder: '',
});
async function fetchPage() {
const res = await api.getPlayers({
page: page.value,
pageSize: pageSize.value,
sortKey: sort.value.sortKey,
sortOrder: sort.value.sortOrder,
});
rows.value = res.data; // just this page
total.value = res.total; // grand total
}
onMounted(fetchPage);
</script>
<template>
<FTGrid
:columns="columns"
:items="rows"
simplePagination
:totalItems="total"
:page="page"
:pageSize="pageSize"
@page-changed="
(p) => {
page = p;
fetchPage();
}
"
@size-changed="
(s) => {
pageSize = s;
page = 1;
fetchPage();
}
"
@sorting-changed="
(s) => {
sort = s;
fetchPage();
}
"
/>
</template>See Server-side pagination for the full contract.
5. Row selection (new capability)
FTTable had no built-in selection — you wired checkboxes by hand. FTGrid gives it to you via the utility column:
vue
<FTGrid
:columns="columns"
:items="rows"
utility="checkbox"
@selection-changed="(selected) => (selectedRows = selected)"
/><FTGrid
:columns="columns"
:items="rows"
utility="checkbox"
@selection-changed="(selected) => (selectedRows = selected)"
/>6. A cell the built-in types can't express
When no cellType fits — an action menu, a progress bar, a multi-line composite — reach for cellRendererOverrides. It accepts a render function or a Vue component keyed by field:
ts
const overrides = {
// render function
actions: (p) => `<button data-id="${p.data.id}">Edit</button>`,
// or a component + params
progress: { component: MyProgressBar, params: { max: 100 } },
};const overrides = {
// render function
actions: (p) => `<button data-id="${p.data.id}">Edit</button>`,
// or a component + params
progress: { component: MyProgressBar, params: { max: 100 } },
};vue
<FTGrid :columns="columns" :items="rows" :cellRendererOverrides="overrides" /><FTGrid :columns="columns" :items="rows" :cellRendererOverrides="overrides" />Gotchas
- No
#rowslot. All cell rendering goes throughcolumns/cellTypeorcellRendererOverrides. If you have lots of irreducibly-custom rows, that's a signal to stay onFTTable. fieldmust match your row keys.cellType: 'status'readsrow[field].- Sorting is per-column. Set
sortable: trueon columns that should sort; in server-side mode the client comparator is neutralised andsorting-changedfires instead. - Filtering is opt-in per column via
filterType: 'set' | 'condition'— it is not automatic the way client sort is. itemsis the current page in server-side mode — do not pass the full dataset alongsidesimplePagination+totalItems.