Skip to content

Column types & formatting ​

Built-in column types ​

The type property configures a column for a specific data type:

TypeAlignmentFilterEditorValue parser
"text" (default)lefttext (contains, starts with, …)text input–
"number"rightnumber (=, <, >, between, …)text input with decimal keyboardconverts "1.234,5" or "1,234.5" to numbers
"date"leftdate (on, before, after, between)date pickerconverts to Date or ISO string (keeps the original data type)
"boolean"centeryes / nocheckboxconverts "true", "yes", "x", "1" to true

Boolean values are rendered as check marks. Types only set defaults, every property can still be overridden in the column definition.

Default column definition ​

Properties which apply to all columns are defined once with the grid option defaultColumn:

ts
const options: GridOptions = {
  defaultColumn: {
    sortable: true,
    filterable: true,
    resizeable: true,
    width: 150,
  },
};

The priority is: column definition > column type > defaultColumn. Column groups (columns with children) do not inherit the default column definition.

Custom column types ​

Reusable sets of properties are defined with columnTypes and referenced by type. A column can have multiple types (type: ["money", "editable"]), later types override earlier ones.

Source
vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" />
</template>

<script setup lang="ts">
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type GridOptions,
} from "@datagrid/vue-ui";

type Product = {
  name: string;
  price: number;
  discount: number;
  stock: number;
  released: string;
  available: boolean;
  tags: Array<string>;
};

const rows: Array<Product> = [
  { name: "Laptop", price: 1299.9, discount: 0.1, stock: 12, released: "2024-02-01", available: true, tags: ["tech", "office"] },
  { name: "Monitor", price: 349, discount: 0, stock: 0, released: "2023-10-15", available: false, tags: ["tech"] },
  { name: "Keyboard", price: 89.5, discount: 0.25, stock: 140, released: "2022-05-30", available: true, tags: ["tech", "accessories"] },
  { name: "Desk", price: 499, discount: 0.05, stock: 7, released: "2021-11-11", available: true, tags: ["furniture"] },
  { name: "Chair", price: 259.99, discount: 0.15, stock: 23, released: "2024-06-20", available: true, tags: ["furniture", "office"] },
];

const columns: Array<ColumnConfig<Product>> = [
  { field: "name", text: "Product" },
  // "money" is a custom column type defined in the options
  { field: "price", text: "Price", type: "money" },
  { field: "discount", text: "Discount", type: "number", valueFormatter: formatters.percent() },
  {
    field: "stock",
    text: "Stock",
    type: "number",
    cellClass: ({ value }) => (value === 0 ? "out-of-stock" : undefined),
  },
  { field: "released", text: "Released", type: "date", valueFormatter: formatters.date({ dateStyle: "long" }) },
  { field: "available", text: "Available", type: "boolean" },
  // arrays are joined with ", "
  { field: "tags", text: "Tags", width: 180 },
  // computed column
  {
    id: "finalPrice",
    text: "Final price",
    type: "money",
    valueGetter: ({ data }) => data.price * (1 - data.discount),
  },
];

const options: GridOptions<Product> = {
  defaultColumn: { sortable: true, filterable: true, width: 140 },
  columnTypes: {
    // a custom type is a reusable set of column properties
    money: {
      align: "right",
      headerAlign: "right",
      filterType: "number",
      editor: "number",
      valueFormatter: formatters.currency("EUR", { locale: "de-DE" }),
    },
  },
};
</script>

<style>
.out-of-stock {
  color: #cf222e;
  font-weight: 600;
}
</style>
tsx
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";

type Product = {
  name: string;
  price: number;
  discount: number;
  stock: number;
  released: string;
  available: boolean;
  tags: Array<string>;
};

const rows: Array<Product> = [
  { name: "Laptop", price: 1299.9, discount: 0.1, stock: 12, released: "2024-02-01", available: true, tags: ["tech", "office"] },
  { name: "Monitor", price: 349, discount: 0, stock: 0, released: "2023-10-15", available: false, tags: ["tech"] },
  { name: "Keyboard", price: 89.5, discount: 0.25, stock: 140, released: "2022-05-30", available: true, tags: ["tech", "accessories"] },
  { name: "Desk", price: 499, discount: 0.05, stock: 7, released: "2021-11-11", available: true, tags: ["furniture"] },
  { name: "Chair", price: 259.99, discount: 0.15, stock: 23, released: "2024-06-20", available: true, tags: ["furniture", "office"] },
];

const columns: Array<ColumnConfig<Product>> = [
  { field: "name", text: "Product" },
  // "money" is a custom column type defined in the options
  { field: "price", text: "Price", type: "money" },
  { field: "discount", text: "Discount", type: "number", valueFormatter: formatters.percent() },
  {
    field: "stock",
    text: "Stock",
    type: "number",
    cellClass: ({ value }) => (value === 0 ? "out-of-stock" : undefined),
  },
  { field: "released", text: "Released", type: "date", valueFormatter: formatters.date({ dateStyle: "long" }) },
  { field: "available", text: "Available", type: "boolean" },
  // arrays are joined with ", "
  { field: "tags", text: "Tags", width: 180 },
  // computed column
  {
    id: "finalPrice",
    text: "Final price",
    type: "money",
    valueGetter: ({ data }) => data.price * (1 - data.discount),
  },
];

const options: GridOptions<Product> = {
  defaultColumn: { sortable: true, filterable: true, width: 140 },
  columnTypes: {
    // a custom type is a reusable set of column properties
    money: {
      align: "right",
      headerAlign: "right",
      filterType: "number",
      editor: "number",
      valueFormatter: formatters.currency("EUR", { locale: "de-DE" }),
    },
  },
};

export default function ColumnTypes() {
  return (
    <>
      <Datagrid columns={columns} rows={rows} options={options} />
      <style>{`
        .out-of-stock {
          color: #cf222e;
          font-weight: 600;
        }
      `}</style>
    </>
  );
}

A custom type with the name of a built-in type (e.g. number) extends the built-in type. A custom type can also reference other types with type, e.g. money: { type: "number", valueFormatter: … } inherits everything from number.

Inheritance and mixin types ​

Custom types pay off as soon as several grids show the same kind of data. The demo defines

  • hours and percent, which inherit alignment, filter, editor and parser from number and only add a formatter,
  • date, which extends the built-in date type, so every date column of the grid gets the same format,
  • editable, a "mixin" type which is combined with other types (type: ["hours", "editable"]) and makes the cells editable with a highlighted background and a ✎ in the header.
Source
vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" />
</template>

<script setup lang="ts">
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";

const rows = createTasks(100);

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", width: 200, type: "editable" },
  { field: "project", text: "Project" },
  // "hours" inherits everything from "number" (alignment, filter, editor, parser)
  { field: "estimate", text: "Estimate", type: ["hours", "editable"] },
  { field: "spent", text: "Spent", type: "hours" },
  { field: "progress", text: "Progress", type: "percent" },
  // "date" extends the built-in type: all date columns get the same format
  { field: "due", text: "Due", type: ["date", "editable"] },
];

const options: GridOptions<Task> = {
  defaultColumn: { sortable: true, filterable: true, width: 130 },
  columnTypes: {
    hours: {
      type: "number",
      valueFormatter: ({ value }) => (value == null ? "" : `${value} h`),
    },
    percent: {
      type: "number",
      valueFormatter: formatters.percent({ maximumFractionDigits: 0 }),
    },
    date: {
      valueFormatter: formatters.date({ day: "2-digit", month: "short", year: "numeric" }),
    },
    // a "mixin" type which can be combined with other types
    editable: {
      editable: true,
      cellClass: "ct-types-editable",
      headerCellClass: "ct-types-editable-header",
    },
  },
};
</script>

<style>
.uni-table .ct-types-editable {
  background-color: color-mix(in srgb, var(--ut-accent-color) 8%, var(--ut-current-row-background));
}
.ct-types-editable-header .ut-header-cell__text::after {
  content: "✎";
  margin-left: 4px;
  color: var(--ut-accent-color);
}
</style>
tsx
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";

const rows = createTasks(100);

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", width: 200, type: "editable" },
  { field: "project", text: "Project" },
  // "hours" inherits everything from "number" (alignment, filter, editor, parser)
  { field: "estimate", text: "Estimate", type: ["hours", "editable"] },
  { field: "spent", text: "Spent", type: "hours" },
  { field: "progress", text: "Progress", type: "percent" },
  // "date" extends the built-in type: all date columns get the same format
  { field: "due", text: "Due", type: ["date", "editable"] },
];

const options: GridOptions<Task> = {
  defaultColumn: { sortable: true, filterable: true, width: 130 },
  columnTypes: {
    hours: {
      type: "number",
      valueFormatter: ({ value }) => (value == null ? "" : `${value} h`),
    },
    percent: {
      type: "number",
      valueFormatter: formatters.percent({ maximumFractionDigits: 0 }),
    },
    date: {
      valueFormatter: formatters.date({ day: "2-digit", month: "short", year: "numeric" }),
    },
    // a "mixin" type which can be combined with other types
    editable: {
      editable: true,
      cellClass: "ct-types-editable",
      headerCellClass: "ct-types-editable-header",
    },
  },
};

export default function ColumnTypesAdvanced() {
  return (
    <>
      <Datagrid columns={columns} rows={rows} options={options} />
      <style>{`
        .uni-table .ct-types-editable {
          background-color: color-mix(in srgb, var(--ut-accent-color) 8%, var(--ut-current-row-background));
        }
        .ct-types-editable-header .ut-header-cell__text::after {
          content: "✎";
          margin-left: 4px;
          color: var(--ut-accent-color);
        }
      `}</style>
    </>
  );
}

Formatting ​

valueFormatter converts a value to the displayed text. The grid ships factories for common formats based on Intl:

ts
import { formatters } from "@datagrid/vue-ui";

const columns = [
  { field: "price", type: "number", valueFormatter: formatters.currency("EUR") },
  { field: "amount", type: "number", valueFormatter: formatters.number({ maximumFractionDigits: 2 }) },
  { field: "share", type: "number", valueFormatter: formatters.percent() },
  { field: "created", type: "date", valueFormatter: formatters.date({ dateStyle: "medium" }) },
  { field: "updated", type: "date", valueFormatter: formatters.dateTime() },
  { field: "done", valueFormatter: formatters.boolean("yes", "no") },
];

All factories accept Intl options and an optional locale. Without locale the language of the browser is used.

The demo shows the factories with different options. Switch the locale to see how numbers, currencies and dates change – the data stays the same:

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      Locale
      <select v-model="locale">
        <option v-for="value in locales" :key="value" :value="value">{{ value }}</option>
      </select>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";

type Order = {
  product: string;
  price: number;
  quantity: number;
  discount: number;
  ordered: string;
  shipped: string;
  updated: string;
  paid: boolean;
};

const locales = ["en-US", "de-DE", "fr-FR", "ja-JP"];
const locale = ref("en-US");

const rows: Array<Order> = [
  { product: "Laptop", price: 1299.9, quantity: 3, discount: 0.1, ordered: "2024-02-01", shipped: "2024-02-03", updated: "2024-02-03T09:15:00", paid: true },
  { product: "Monitor", price: 349, quantity: 12, discount: 0, ordered: "2024-03-15", shipped: "2024-03-20", updated: "2024-03-21T16:40:00", paid: false },
  { product: "Keyboard", price: 89.5, quantity: 1250, discount: 0.255, ordered: "2024-04-30", shipped: "2024-05-02", updated: "2024-05-02T11:05:00", paid: true },
  { product: "Desk", price: 499, quantity: 7, discount: 0.05, ordered: "2024-06-11", shipped: "2024-06-18", updated: "2024-06-19T08:30:00", paid: false },
  { product: "Chair", price: 259.99, quantity: 23, discount: 0.15, ordered: "2024-07-20", shipped: "2024-07-22", updated: "2024-07-22T14:55:00", paid: true },
];

// formatters are created once per locale (Intl formats are expensive to create)
const columns = computed<Array<ColumnConfig<Order>>>(() => [
  { field: "product", text: "Product" },
  { field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR", { locale: locale.value }) },
  { field: "quantity", text: "Quantity", type: "number", valueFormatter: formatters.number({ locale: locale.value }) },
  { field: "discount", text: "Discount", type: "number", valueFormatter: formatters.percent({ locale: locale.value, maximumFractionDigits: 1 }) },
  { field: "ordered", text: "Ordered (short)", type: "date", valueFormatter: formatters.date({ locale: locale.value, dateStyle: "short" }) },
  { field: "shipped", text: "Shipped (custom)", type: "date", valueFormatter: formatters.date({ locale: locale.value, weekday: "short", day: "numeric", month: "short" }) },
  { field: "updated", text: "Updated", type: "date", width: 190, valueFormatter: formatters.dateTime({ locale: locale.value, dateStyle: "medium", timeStyle: "short" }) },
  { field: "paid", text: "Paid", align: "center", valueFormatter: formatters.boolean("✅ paid", "⏳ open") },
]);

const options: GridOptions<Order> = {
  defaultColumn: { sortable: true, width: 140 },
};
</script>
tsx
import { useMemo, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";

type Order = {
  product: string;
  price: number;
  quantity: number;
  discount: number;
  ordered: string;
  shipped: string;
  updated: string;
  paid: boolean;
};

const locales = ["en-US", "de-DE", "fr-FR", "ja-JP"];

const rows: Array<Order> = [
  { product: "Laptop", price: 1299.9, quantity: 3, discount: 0.1, ordered: "2024-02-01", shipped: "2024-02-03", updated: "2024-02-03T09:15:00", paid: true },
  { product: "Monitor", price: 349, quantity: 12, discount: 0, ordered: "2024-03-15", shipped: "2024-03-20", updated: "2024-03-21T16:40:00", paid: false },
  { product: "Keyboard", price: 89.5, quantity: 1250, discount: 0.255, ordered: "2024-04-30", shipped: "2024-05-02", updated: "2024-05-02T11:05:00", paid: true },
  { product: "Desk", price: 499, quantity: 7, discount: 0.05, ordered: "2024-06-11", shipped: "2024-06-18", updated: "2024-06-19T08:30:00", paid: false },
  { product: "Chair", price: 259.99, quantity: 23, discount: 0.15, ordered: "2024-07-20", shipped: "2024-07-22", updated: "2024-07-22T14:55:00", paid: true },
];

function createColumns(locale: string): Array<ColumnConfig<Order>> {
  return [
    { field: "product", text: "Product" },
    { field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR", { locale }) },
    { field: "quantity", text: "Quantity", type: "number", valueFormatter: formatters.number({ locale }) },
    { field: "discount", text: "Discount", type: "number", valueFormatter: formatters.percent({ locale, maximumFractionDigits: 1 }) },
    { field: "ordered", text: "Ordered (short)", type: "date", valueFormatter: formatters.date({ locale, dateStyle: "short" }) },
    { field: "shipped", text: "Shipped (custom)", type: "date", valueFormatter: formatters.date({ locale, weekday: "short", day: "numeric", month: "short" }) },
    { field: "updated", text: "Updated", type: "date", width: 190, valueFormatter: formatters.dateTime({ locale, dateStyle: "medium", timeStyle: "short" }) },
    { field: "paid", text: "Paid", align: "center", valueFormatter: formatters.boolean("✅ paid", "⏳ open") },
  ];
}

const options: GridOptions<Order> = {
  defaultColumn: { sortable: true, width: 140 },
};

export default function ColumnFormatters() {
  const [locale, setLocale] = useState("en-US");
  // formatters are created once per locale (Intl formats are expensive to create)
  const columns = useMemo(() => createColumns(locale), [locale]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        Locale
        <select value={locale} onChange={(e) => setLocale(e.target.value)}>
          {locales.map((value) => (
            <option key={value} value={value}>
              {value}
            </option>
          ))}
        </select>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} />
    </div>
  );
}

Create the columns (and with them the formatters) again only when the locale changes: Intl formats are expensive to create, a formatter reuses its format for all cells.

Without formatter:

  • null and undefined are displayed as empty cells
  • Date objects are displayed with toLocaleDateString() (or toLocaleString() if they contain a time)
  • arrays are joined with ", "
  • objects are displayed as JSON

A custom formatter is a simple function:

ts
{
  field: "size",
  valueFormatter: ({ value }) => (value == null ? "" : `${(value / 1024).toFixed(1)} KB`),
}

Formatter vs. slot

Use a formatter if the cell shows text. The formatted text is also used by the quick filter, the column autosize and the export. Use a slot or component for rich content like icons, badges or buttons.

Text wrapping and auto height ​

Cells show a single line and cut long text with an ellipsis. autoHeight: true wraps the text of a column and the row grows with its content – useful for descriptions, comments or addresses. Line breaks in the value are kept. Columns with autoHeight are edited with a multi line editor (textarea, Shift + Enter for new lines).

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label><input v-model="wrap" type="checkbox" /> Wrap text (<code>autoHeight</code>)</label>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";

type Ticket = { id: number; subject: string; description: string; customer: string };

const rows: Array<Ticket> = [
  { id: 1041, subject: "Login fails", customer: "Acme Corp", description: "Users with an umlaut in their password cannot log in since the last update. The error message says 'invalid credentials'." },
  { id: 1042, subject: "Invoice PDF", customer: "Globex", description: "The logo is missing." },
  { id: 1043, subject: "Export to Excel", customer: "Initech", description: "Dates are exported as text, so they cannot be sorted in Excel. Numbers are fine. It would also be great to keep the column widths of the grid." },
  { id: 1044, subject: "Dark mode", customer: "Umbrella", description: "Please add a dark mode." },
  { id: 1045, subject: "Slow search", customer: "Hooli", description: "Searching in the customer list takes several seconds when more than 10,000 customers exist. It was fast in version 2.3, the problem started with version 2.4.\nSteps: open customers, type a name." },
  { id: 1046, subject: "Typo", customer: "Stark Industries", description: "'Adress' in the settings dialog." },
];

const wrap = ref(true);

const columns = computed<Array<ColumnConfig<Ticket>>>(() => [
  { field: "id", text: "#", type: "number", width: 70 },
  { field: "subject", text: "Subject", width: 150 },
  // autoHeight: the text wraps and the row grows with the content
  { field: "description", text: "Description", flex: 1, minWidth: 200, autoHeight: wrap.value, editable: true },
  { field: "customer", text: "Customer", width: 140 },
]);

const options: GridOptions<Ticket> = {
  // no fixed rowHeight: rows are measured
  defaultColumn: { resizeable: true },
};
</script>
tsx
import { useMemo, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";

type Ticket = { id: number; subject: string; description: string; customer: string };

const rows: Array<Ticket> = [
  { id: 1041, subject: "Login fails", customer: "Acme Corp", description: "Users with an umlaut in their password cannot log in since the last update. The error message says 'invalid credentials'." },
  { id: 1042, subject: "Invoice PDF", customer: "Globex", description: "The logo is missing." },
  { id: 1043, subject: "Export to Excel", customer: "Initech", description: "Dates are exported as text, so they cannot be sorted in Excel. Numbers are fine. It would also be great to keep the column widths of the grid." },
  { id: 1044, subject: "Dark mode", customer: "Umbrella", description: "Please add a dark mode." },
  { id: 1045, subject: "Slow search", customer: "Hooli", description: "Searching in the customer list takes several seconds when more than 10,000 customers exist. It was fast in version 2.3, the problem started with version 2.4.\nSteps: open customers, type a name." },
  { id: 1046, subject: "Typo", customer: "Stark Industries", description: "'Adress' in the settings dialog." },
];

const options: GridOptions<Ticket> = {
  // no fixed rowHeight: rows are measured
  defaultColumn: { resizeable: true },
};

export default function ColumnAutoHeight() {
  const [wrap, setWrap] = useState(true);

  const columns = useMemo<Array<ColumnConfig<Ticket>>>(
    () => [
      { field: "id", text: "#", type: "number", width: 70 },
      { field: "subject", text: "Subject", width: 150 },
      // autoHeight: the text wraps and the row grows with the content
      { field: "description", text: "Description", flex: 1, minWidth: 200, autoHeight: wrap, editable: true },
      { field: "customer", text: "Customer", width: 140 },
    ],
    [wrap]
  );

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          <input type="checkbox" checked={wrap} onChange={(e) => setWrap(e.target.checked)} />
          {" Wrap text ("}<code>autoHeight</code>)
        </label>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} />
    </div>
  );
}

Auto height requires measured rows, so don't set a fixed rowHeight. See Row height.

Released under the ISC License.