<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fernando Arturi</title>
    <description>The latest articles on DEV Community by Fernando Arturi (@fernando_arturi_8cd587e02).</description>
    <link>https://dev.to/fernando_arturi_8cd587e02</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4199158%2F7d66db09-54a6-4d5c-a3ec-123172602781.jpg</url>
      <title>DEV Community: Fernando Arturi</title>
      <link>https://dev.to/fernando_arturi_8cd587e02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fernando_arturi_8cd587e02"/>
    <language>en</language>
    <item>
      <title>inandu-grid-vue: a free, MIT-licensed data grid for Vue 3 (grouping, inline editing, server-side paging)</title>
      <dc:creator>Fernando Arturi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:57:45 +0000</pubDate>
      <link>https://dev.to/fernando_arturi_8cd587e02/inandu-grid-vue-a-free-mit-licensed-data-grid-for-vue-3-grouping-inline-editing-server-side-1ian</link>
      <guid>https://dev.to/fernando_arturi_8cd587e02/inandu-grid-vue-a-free-mit-licensed-data-grid-for-vue-3-grouping-inline-editing-server-side-1ian</guid>
      <description>&lt;p&gt;Most Vue projects hit the same wall sooner or later: a plain &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;I've been building &lt;strong&gt;inandu-grid-vue&lt;/strong&gt;, 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 &lt;code&gt;vue&lt;/code&gt; as its only peer dependency. This post walks through how it works, with code you can paste.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://inandusolutions.github.io/inandu-grid-vue/" rel="noopener noreferrer"&gt;https://inandusolutions.github.io/inandu-grid-vue/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit it in StackBlitz:&lt;/strong&gt; &lt;a href="https://stackblitz.com/github/inandusolutions/inandu-grid-vue/tree/main/examples/stackblitz" rel="noopener noreferrer"&gt;https://stackblitz.com/github/inandusolutions/inandu-grid-vue/tree/main/examples/stackblitz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@inandu-solutions/grid-vue" rel="noopener noreferrer"&gt;&lt;code&gt;@inandu-solutions/grid-vue&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/inandusolutions/inandu-grid-vue" rel="noopener noreferrer"&gt;https://github.com/inandusolutions/inandu-grid-vue&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://inandusolutions.github.io/inandu-grid-vue/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2vnoqwvb54deepob8bt.gif" alt="Free-text search filtering the grid live" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and render a grid
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @inandu-solutions/grid-vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InanduGrid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InanduGridColumn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inandu-solutions/grid-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inandu-solutions/grid-vue/style.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ada&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;41&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InanduGridColumn&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;age&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Age&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- sorting + filtering are on by default --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;InanduGrid&lt;/span&gt; &lt;span class="na"&gt;:rows=&lt;/span&gt;&lt;span class="s"&gt;"rows"&lt;/span&gt; &lt;span class="na"&gt;:columns=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt; &lt;span class="na"&gt;:page-size=&lt;/span&gt;&lt;span class="s"&gt;"25"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns are plain objects. &lt;code&gt;type&lt;/code&gt; (&lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;) 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grouping and aggregates
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InanduGridColumn&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Team&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Salary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0-0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;avg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://inandusolutions.github.io/inandu-grid-vue/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6xv6ahm0n11yka2xd9i.gif" alt="Grouping a column live in the demo grid" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline editing, without the grid owning your data
&lt;/h2&gt;

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shallowRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InanduGrid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InanduGridColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InanduGridRowSave&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inandu-solutions/grid-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shallowRef&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ada&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90000&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InanduGridColumn&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Salary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRowSave&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;InanduGridRowSave&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;InanduGrid&lt;/span&gt; &lt;span class="na"&gt;:rows=&lt;/span&gt;&lt;span class="s"&gt;"rows"&lt;/span&gt; &lt;span class="na"&gt;:columns=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt; &lt;span class="na"&gt;deletable&lt;/span&gt; &lt;span class="na"&gt;creatable&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;row-save=&lt;/span&gt;&lt;span class="s"&gt;"onRowSave"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Slots for cells, headers, editors and detail rows
&lt;/h2&gt;

&lt;p&gt;Scoped slots replace the built-in rendering per column (&lt;code&gt;#cell-&amp;lt;field&amp;gt;&lt;/code&gt;, &lt;code&gt;#header-&amp;lt;field&amp;gt;&lt;/code&gt;, &lt;code&gt;#editor-&amp;lt;field&amp;gt;&lt;/code&gt;), and a &lt;code&gt;#detail&lt;/code&gt; slot adds an expandable master-detail row. Sorting, resizing and validation keep working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;InanduGrid&lt;/span&gt; &lt;span class="na"&gt;:rows=&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt; &lt;span class="na"&gt;:columns=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;#cell-status&lt;/span&gt;&lt;span class="err"&gt;="{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="err"&gt;}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;StatusBadge&lt;/span&gt; &lt;span class="na"&gt;:status=&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;#detail&lt;/span&gt;&lt;span class="err"&gt;="{&lt;/span&gt; &lt;span class="na"&gt;row&lt;/span&gt; &lt;span class="err"&gt;}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OrderLines&lt;/span&gt; &lt;span class="na"&gt;:order=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/InanduGrid&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather work in render functions, each column also accepts &lt;code&gt;renderCell&lt;/code&gt;, &lt;code&gt;renderHeader&lt;/code&gt; and &lt;code&gt;renderEditor&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten thousand rows: virtual scroll
&lt;/h2&gt;

&lt;p&gt;Pagination is the default, but for long lists you can render only what's visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;InanduGrid&lt;/span&gt;
  &lt;span class="na"&gt;:rows=&lt;/span&gt;&lt;span class="s"&gt;"tenThousandRows"&lt;/span&gt;
  &lt;span class="na"&gt;:columns=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt;
  &lt;span class="na"&gt;virtual-scroll&lt;/span&gt;
  &lt;span class="na"&gt;:virtual-row-height=&lt;/span&gt;&lt;span class="s"&gt;"40"&lt;/span&gt;
  &lt;span class="na"&gt;:height=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-side mode
&lt;/h2&gt;

&lt;p&gt;When the table is too big to load, let the backend sort, filter and page while the UI stays the same. &lt;code&gt;useInanduGridServer&lt;/code&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InanduGrid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useInanduGridServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inandu-solutions/grid-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInanduGridServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;InanduGrid&lt;/span&gt;
    &lt;span class="na"&gt;:rows=&lt;/span&gt;&lt;span class="s"&gt;"server.rows.value"&lt;/span&gt;
    &lt;span class="na"&gt;:columns=&lt;/span&gt;&lt;span class="s"&gt;"columns"&lt;/span&gt;
    &lt;span class="na"&gt;:page-size=&lt;/span&gt;&lt;span class="s"&gt;"25"&lt;/span&gt;
    &lt;span class="na"&gt;:server-side=&lt;/span&gt;&lt;span class="s"&gt;"{ totalItems: server.total.value }"&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;query-change=&lt;/span&gt;&lt;span class="s"&gt;"server.setQuery"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request looks like &lt;code&gt;/api/products?page=2&amp;amp;pageSize=25&amp;amp;sort=-price&amp;amp;q=cable&amp;amp;price_gte=20&lt;/code&gt; and the response is &lt;code&gt;{ data: [...], total: n }&lt;/code&gt;. The &lt;a href="https://github.com/inandusolutions/inandu-grid-extensions/blob/main/docs/request-contract.md" rel="noopener noreferrer"&gt;request contract&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headless: just the state
&lt;/h2&gt;

&lt;p&gt;If you want your own markup, &lt;code&gt;useInanduGrid()&lt;/code&gt; 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 &lt;code&gt;.value&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headless&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInanduGrid&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibleRows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// the current page, already sorted and filtered&lt;/span&gt;
&lt;span class="nx"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggleSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// drive it from your own UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component itself exposes the same engine on a template ref (&lt;code&gt;gridRef.value?.grid.toggleSort('name')&lt;/code&gt;) for programmatic control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the list
&lt;/h2&gt;

&lt;p&gt;Briefly, because it's all in the README:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Where it comes from
&lt;/h2&gt;

&lt;p&gt;The framework-agnostic core (sorting, filtering, aggregation, export, i18n) is ported from an Angular grid, &lt;a href="https://github.com/inandusolutions/inandu-grid" rel="noopener noreferrer"&gt;&lt;code&gt;@inandu-solutions/grid-angular&lt;/code&gt;&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Play with the &lt;a href="https://inandusolutions.github.io/inandu-grid-vue/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;; every tab is the real component.&lt;/li&gt;
&lt;li&gt;Open the &lt;a href="https://stackblitz.com/github/inandusolutions/inandu-grid-vue/tree/main/examples/stackblitz" rel="noopener noreferrer"&gt;StackBlitz example&lt;/a&gt; and edit &lt;code&gt;src/App.vue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm install @inandu-solutions/grid-vue&lt;/code&gt;, then see the &lt;a href="https://github.com/inandusolutions/inandu-grid-vue#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; for the full API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT, issues and PRs are welcome on &lt;a href="https://github.com/inandusolutions/inandu-grid-vue/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and I'd like to hear which feature you hit first that the grid doesn't cover.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
