inandu-grid-vue: a free, MIT-licensed data grid for Vue 3 (grouping, inline editing, server-side paging)

inandu-grid-vue: a free, MIT-licensed data grid for Vue 3 (grouping, inline editing, server-side paging)

# vue# typescript# webdev# opensource
inandu-grid-vue: a free, MIT-licensed data grid for Vue 3 (grouping, inline editing, server-side paging)Fernando Arturi

Most Vue projects hit the same wall sooner or later: a plain <table> stops being enough. You...

Most Vue projects hit the same wall sooner or later: a plain <table> stops being enough. You need sorting, a search box, per-column filters, grouping with totals, inline editing with validation, an export button, and eventually a table too big to load at once.

I've been building inandu-grid-vue, a Vue 3 data grid that covers that list without an enterprise tier. It's MIT-licensed, ships as a single component plus a headless composable, and is about 19 KB gzipped (JS) plus 2 KB of CSS, with vue as its only peer dependency. This post walks through how it works, with code you can paste.

Free-text search filtering the grid live

Install and render a grid

npm install @inandu-solutions/grid-vue
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
import { InanduGrid, type InanduGridColumn } from '@inandu-solutions/grid-vue';
import '@inandu-solutions/grid-vue/style.css';

const rows = [{ name: 'Ada', age: 36 }, { name: 'Alan', age: 41 }];
const columns: InanduGridColumn[] = [
  { field: 'name', headerText: 'Name' },
  { field: 'age', headerText: 'Age', type: 'number' },
];
</script>

<template>
  <!-- sorting + filtering are on by default -->
  <InanduGrid :rows="rows" :columns="columns" :page-size="25" />
</template>
Enter fullscreen mode Exit fullscreen mode

Columns are plain objects. type (string, number, date, boolean) decides the filter control, the formatting and the sort rule, so a number column gets a min/max filter and a date column gets a from/to range without extra code. The default stylesheet is optional; skip the import and bring your own CSS if you prefer.

Grouping and aggregates

Declare an aggregate on a column and pick a column in the grid's group-by control. You get per-group sum / avg / min / max / count and a grand total row:

const columns: InanduGridColumn[] = [
  { field: 'team', headerText: 'Team' },
  { field: 'salary', headerText: 'Salary', type: 'number', format: '1.0-0', aggregate: 'avg' },
];
Enter fullscreen mode Exit fullscreen mode

Grouping a column live in the demo grid

Inline editing, without the grid owning your data

Mark columns editable, add creatable / deletable, and the grid renders the edit controls and validates at save time (required, min, max, pattern, a custom validator, and asyncValidator).

The design choice I care most about: the grid never mutates rows. Events hand you the change, and you decide what to do with it (update local state, call an API, roll back):

<script setup lang="ts">
import { shallowRef } from 'vue';
import { InanduGrid, type InanduGridColumn, type InanduGridRowSave } from '@inandu-solutions/grid-vue';

const rows = shallowRef([{ id: 1, name: 'Ada', salary: 90000 }]);
const columns: InanduGridColumn[] = [
  { field: 'name', headerText: 'Name', editable: true, required: true },
  { field: 'salary', headerText: 'Salary', type: 'number', editable: true, min: 0 },
];

function onRowSave({ row, values }: InanduGridRowSave) {
  rows.value = rows.value.map(r => (r === row ? { ...r, ...values } : r));
}
</script>

<template>
  <InanduGrid :rows="rows" :columns="columns" deletable creatable @row-save="onRowSave" />
</template>
Enter fullscreen mode Exit fullscreen mode

If you've ever fought a component that edits your array behind your back, this is the opposite. There are matching row-create, row-delete and rows-delete events.

Slots for cells, headers, editors and detail rows

Scoped slots replace the built-in rendering per column (#cell-<field>, #header-<field>, #editor-<field>), and a #detail slot adds an expandable master-detail row. Sorting, resizing and validation keep working:

<InanduGrid :rows="orders" :columns="columns">
  <template #cell-status="{ value }">
    <StatusBadge :status="value" />
  </template>
  <template #detail="{ row }">
    <OrderLines :order="row" />
  </template>
</InanduGrid>
Enter fullscreen mode Exit fullscreen mode

If you'd rather work in render functions, each column also accepts renderCell, renderHeader and renderEditor.

Ten thousand rows: virtual scroll

Pagination is the default, but for long lists you can render only what's visible:

<InanduGrid
  :rows="tenThousandRows"
  :columns="columns"
  virtual-scroll
  :virtual-row-height="40"
  :height="400"
/>
Enter fullscreen mode Exit fullscreen mode

You give it a fixed row height and a viewport height. The row height isn't auto-measured, which keeps the scroll math simple and predictable. Sorting, search and filters still work over the full set.

Server-side mode

When the table is too big to load, let the backend sort, filter and page while the UI stays the same. useInanduGridServer turns the grid's state into a query string, aborts requests that have been superseded, debounces typing, and keeps the previous page on screen while the next one loads:

<script setup lang="ts">
import { InanduGrid, useInanduGridServer } from '@inandu-solutions/grid-vue';

const server = useInanduGridServer({ url: '/api/products' });
</script>

<template>
  <InanduGrid
    :rows="server.rows.value"
    :columns="columns"
    :page-size="25"
    :server-side="{ totalItems: server.total.value }"
    @query-change="server.setQuery"
  />
</template>
Enter fullscreen mode Exit fullscreen mode

The request looks like /api/products?page=2&pageSize=25&sort=-price&q=cable&price_gte=20 and the response is { data: [...], total: n }. The request contract is plain query-string parameters, so any backend can implement it. If yours is ASP.NET Core with EF Core, there's a companion package that does it in one call, but nothing on the Vue side depends on it.

Headless: just the state

If you want your own markup, useInanduGrid() gives you the whole engine without any DOM. Pass a getter so it follows reactive data; every piece of state is a reactive getter, so there's no .value:

const headless = useInanduGrid(() => ({ rows: rows.value, columns, pageSize: 25 }));

headless.visibleRows;        // the current page, already sorted and filtered
headless.toggleSort('name'); // drive it from your own UI
Enter fullscreen mode Exit fullscreen mode

The component itself exposes the same engine on a template ref (gridRef.value?.grid.toggleSort('name')) for programmatic control.

The rest of the list

Briefly, because it's all in the README:

  • Export: CSV, Excel (.xls) and PDF, plus print, from one exportable flag. jspdf is loaded lazily, so you only pay for the PDF path if someone uses it.
  • Columns: drag-resize, double-click to autosize, drag-reorder, pin left or right, show/hide at runtime.
  • Selection: row selection, Excel-style cell range selection (cell-range-selection, multi-range), and clipboard copy/paste as TSV.
  • Tree data: point tree-children-key at the property holding each row's children.
  • i18n: English, Spanish, French, Italian and Chinese are bundled; switch with lang="es".
  • TypeScript: props, events and column definitions are typed (InanduGridColumn, InanduGridRowSave, ...).

Where it comes from

The framework-agnostic core (sorting, filtering, aggregation, export, i18n) is ported from an Angular grid, @inandu-solutions/grid-angular, and kept in sync by hand across the Angular, React and Vue repos, so a fix to the core lands in all three. The Vue layer is where the Vue-specific parts live: the component, SFC slots, emitted events and the composables.

Try it

  • Play with the live demo; every tab is the real component.
  • Open the StackBlitz example and edit src/App.vue.
  • npm install @inandu-solutions/grid-vue, then see the README for the full API.

It's MIT, issues and PRs are welcome on GitHub, and I'd like to hear which feature you hit first that the grid doesn't cover.