Skip to content

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 FTTable code to scaffold the migration.

FTGrid is not a drop-in replacement for FTTable. They solve different problems:

  • FTTable is slot-driven — you template every cell in markup. Great for small, bespoke lists where each row is hand-crafted.
  • FTGrid is config-driven — you describe columns with a cellType, 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

FTTableFTGridNotes
heads: (string | FTTableHead)[]columns: IFTGridColumnDef[]Columns now carry cellType + per-type options
#row / #head slotscolumns config (+ cellRendererOverrides)Cell rendering moves from markup to config
itemsitemsSame — array of row objects
withPagingalways paginatedPagination is built in; no flag needed
defaultPageSizepageSizeInitial rows per page
defaultSortKey / defaultSortOrdercolumn sortable + click-to-sortClient sort is automatic per sortable column
totalItemstotalItemsSame — server's grand total
simplePaginationsimplePaginationSame — the server-side switch
pagepageSame — controlled current page
noStripes / isWrappedrowStyle ('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:

FTTableFTGridPayload
page-changedpage-changedpage: number
size-changedsize-changedsize: number
sorting-changedsorting-changed{ sortKey: string; sortOrder: 'asc' | 'desc' | '' }
selection-changedrows[] — 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 customcellRendererOverrides — 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 #row slot. All cell rendering goes through columns/cellType or cellRendererOverrides. If you have lots of irreducibly-custom rows, that's a signal to stay on FTTable.
  • field must match your row keys. cellType: 'status' reads row[field].
  • Sorting is per-column. Set sortable: true on columns that should sort; in server-side mode the client comparator is neutralised and sorting-changed fires instead.
  • Filtering is opt-in per column via filterType: 'set' | 'condition' — it is not automatic the way client sort is.
  • items is the current page in server-side mode — do not pass the full dataset alongside simplePagination + totalItems.