Changelog
All notable changes to @dynamicforms/vue-grid will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.8.1] - 2026-10-21
Added
view-changeevent withGridViewChangeEvent: emitted when the grid is created and whenever what it displays changes. It carries the sort state in effect, the filter values, the number of records shown and loaded, whether filtering and sorting are local, andreasons(initial,filter,sort,records,columns).recordCountprop (GridRecordCount:matching, optionaltotal) for counts known only to the backend.- The default status bar shows the record count next to the active filter count; new translation keys
RecordCountandRecordCountOfTotal. ThestatusBarslot also receivesview(GridViewState) andrecordCount.
[0.8.0] - 2026-10-20
Changed
- Peer dependencies:
@dynamicforms/vuetify-inputs^0.15.0and@dynamicforms/vue-forms^3.0.0-alpha.3. Both report namespaced error codes (vuetify-inputs.<code>,vue-forms.<code>); vue-grid reads no error codes itself.
[0.7.2] - 2026-10-20
Added
link-fast,email-fast,file-fastandnull-null-fastrenderers: they render the<a>or the framednullas the cell itself, one element less per cell. The element is stretched to the cell's grid area: the whole cell is the link, a click anywhere in afile-fastcell does not reach the row, and thenullframe is drawn around the whole cell.setAllowedUrlSchemes(),getAllowedUrlSchemes()anddefaultUrlSchemes: the URL schemes a link in a cell may take, for every grid; the grids render their links again when they are set. TheurlSchemesrenderer option oflink,email,fileand their-fastvariants sets them for one column. The default remainshttp,https,mailtoandtel.- The Built-in Renderers example shows every built-in renderer.
Fixed
color: the cell keeps the size of its grid area and the swatch is a 1.25em square centred vertically in it. The renderer's element is the cell itself, so the swatch's size andbackground-colorwere applied to the whole cell: it was not square and sat at the top of the row. The value is now passed as the--df-cell-colorcustom property and drawn by the cell's::before.link,email,file: the<a>is put in adivthat is the cell, so the link is as wide as its text. The<a>was the cell itself, stretched to its grid area, and a click anywhere in afilecell did not reach the row.null-null: the text is put in aspanwith the CSS classdf-cell-null, in adivthat is the cell, so the frame is drawn around the text. The class was on the cell, and the frame was drawn around the whole cell.
[0.7.1] - 2026-10-13
Fixed
- The grid no longer throws when
recordsshrinks below the first mounted row while the body is scrolled down, e.g. when the records are cleared or filtered after more rows have been loaded.
[0.7.0] - 2026-10-13
Changed
- Breaking: requires
@dynamicforms/vue-forms3.0 and@dynamicforms/vuetify-inputs0.14.1 (peerDependencies^3.0.0-alpha.1and^0.14.1).RenderableValue,SimpleComponentDef,MdStringandMessagesWidget, which cell renderers return and the grid renders with, are imported from@dynamicforms/vuetify-inputs. - Breaking: requires
@dynamicforms/translatable0.3 (peerDependencies^0.3.0).translateStrings(t, namespace?)takes the application's translation function, shaped like vue-i18n'st, in place of a callback returning templates; the translation function substitutes the placeholders.translatableStringsis removed;translate(key, params?)returns the current text of a key. - Breaking: a renderer passed to
setCellRenderer()returns HTML as anHtmlStringfrom@dynamicforms/vuetify-inputs;SimpleComponentDefhas nocomponentVHtml. - Breaking: a cell value, and a string returned by
transform,preRenderorpostRender, is rendered as text.int,float,decimal,ip4,ip6andipescape the value in the HTML they build;link,email,fileandcolorrender an element with the value as a prop.link,emailandfilerender a URL whose scheme is nothttp,https,mailtoortelas text.fileopens the file withtarget="_blank"and stops the click in a handler, without an inline script. HTML is returned as anHtmlString.
[0.6.2] - 2026-10-02
Added
reMeasureLayouts()exposed method: measures the width every responsive layout needs again from the records sampled now and selects the widest layout that fits. The returned promise resolves once the layout has been selected.
Fixed
- Responsive layouts measured before the records arrived kept the width measured from the header alone, so a grid whose first page loaded asynchronously stayed on a layout too wide for its container. A layout is now measured again whenever more records are available to sample than it was measured from.
[0.6.1] - 2026-10-02
Fixed
- The sorting indicator in a header cell with
text-right(or any right/endtext-align) sat past the label at the cell's right edge, overflowing it; it now follows the label as in left-aligned columns. - Columns sized by their header came out narrower than the header cell by its horizontal padding and border, so a header label and its sorting indicator overlapped in a narrow column.
- A non-sortable column's header reserved room for a sorting indicator it never draws, widening columns sized by their header;
SortingIndicatornow renders nothing whensortableis false. - A header cell whose column carries
text-right,text-endortext-centerkept its label at the start of the cell; the label and sorting indicator now follow that alignment. - The hidden header copies that size the columns and pick the responsive layout did not receive styling set on
.df-grid.header, so a bold header label was measured in the regular weight and wrapped in a column sized for it. Both copies now carry thedf-grid headerclasses.
[0.6.0] - 2026-10-01
Changed
- Breaking: requires
@dynamicforms/vue-forms2.0.2 and@dynamicforms/vuetify-inputs0.12.1 (peerDependencies^2.0.2and^0.12.1). A filter field'saccessdecides what it contributes tofilterValues:'disabled'leaves it out, as a disabled field was left out before, and'disabled-null'sendsnullwhatever it holds. Neither filters locally or counts as an active filter in the status bar, and while everyfilterExternalcolumn's field is'disabled'the grid filters locally. A field'svisibilitychanges none of this. update:filterStateandfilterare emitted when the filter state's value becomes empty - every filter field disabled, or afilterStatewithout filterable columns passed in - withfilterValues: {}. Before, nothing was emitted, so an application filtering server-side kept the last filters it was sent.
[0.5.2] - 2026-09-11
Added
.df-grid.cellnow defaults togrid-row: calc(var(--row-base) + 1)— a single-row-per-record layout needs nogrid-rowdeclaration at all; a multi-row layout still overrides it per field for rows after the first.--card-positionCSS custom property, read by.df-grid.card'sposition(defaultabsolute). Set it tostaticorrelativeon any ancestor to opt the row-anchor into a real, in-flow box — padding and margin on.df-grid.cardthen do something, though padding grows the row's own track height rather than insetting cell content, since it's still a sibling of the cells, not their container. Only safe for a layout where every cell has an explicitgrid-column: an auto-placedgrid-columncollides with an in-flow row-anchor spanning the full row, the same collisionabsoluteexists to avoid. See Row-anchor position.
Changed
.df-grid.body-gridnow defaults toalign-content: start. The body scroller has a definite height of its own, so itsauto-sized row tracks were previously subject toalign-content's own default ofstretch: with fewer rows than the container's height needs, or a container taller than its content, every row track grew by an equal share of the leftover space instead of leaving it below the last row — inflating every row well past its real content height. Overridealign-contenton.df-grid.body-gridif you want the leftover space distributed some other way. See Card layout CSS.
[0.5.1] - 2026-09-10
Added
topInsertedPksonUseRecentlyAdded(useRecentlyAdded's return value): the grid now shiftsscrollTopby the height (plus the surroundingrow-gap) of records that land above the visible viewport, so rows already on screen stay in place instead of visually sliding down when content is prepended above them.
[0.5.0] - 2026-09-07
Changed
- Every row is now a direct item of one shared
display: gridinstead of being its own independent grid, and column widths are resolved by the browser natively instead of being measured on a hidden shadow-grid copy and broadcast via a CSS variable. Consuming CSS that declared.df-grid.card { display: grid; grid-template-columns: ...; }must move that declaration to.df-record-gridinstead — the marker every place that lays out a record's fields carries (the real scrolling body, the header, and the filter row alike), so one declaration covers all three instead of just the body..df-grid.cardis now the row-anchor: a styleable but otherwise empty box, not a grid container. Every layout, including a plain single row per record, must now give each cell an explicitgrid-row: calc(var(--row-base) + N)rather than an absolute row number or none at all — plain CSS auto-placement has no notion of record boundaries once rows share a grid, so an unplaced cell'sgrid-row: autokeeps advancing across the whole grid instead of restarting per record. A layout with more than one row of fields per record must additionally declarerowson itsResponsiveColumnDefinition. See Card layout CSS and the Cookbook. - Row virtualization no longer depends on
@pdanpdan/virtual-scroll(dropped as a peer dependency); windowing is now a small internal composable with the same externally-visible behaviour (load,recentlyAdded's viewport signal, the scrollbar-width measurement).
Fixed
- The secondary shadow grids used to pick which responsive layout is active could report a layout's required width as far wider than it actually needs, whenever that layout's columns have no fixed upper bound (e.g. free-text fields laid out with
autoorfrtracks): the measurement was always taken at each field's natural, single-line width, so one long sampled value could make an otherwise-comfortable layout look too wide to pick, falling back to a narrower one than necessary. Each candidate is now also measured with every field forced to wrap at every opportunity, and the median number of lines the sample actually wraps into — not the worst case — shrinks the reported width back down for fields whose typical content wraps comfortably, so a rare long outlier no longer forces the whole layout to look wider than it needs to be. No API changed; layouts using unboundedauto/frtracks for free-text columns are simply picked more accurately. - A record's
--row-baseand the windowing spacers'grid-rowwere computed from the record's absolute index in the full dataset (recordIndex * rows), so the number of CSS grid row lines actually referenced grew with the dataset's total size rather than with how many rows were mounted. Firefox stops generating further implicit grid row tracks past roughly 10,000 of them, silently collapsing every row beyond that onto the same line — on a large enough dataset (around 3,300 records for a three-row layout, fewer for a layout with more rows per record), every row scrolled past that point rendered on top of the last working one.--row-baseis now computed from a record's position within the currently-mounted window instead, which keeps the grid lines actually used bounded byminRenderedRowsregardless of the dataset's total size. Chromium was not observed to have this limit, so this was invisible there. - A single-row layout that relies on plain CSS column auto-placement (no explicit
grid-columnon any cell) had every cell overflow into newly-created implicit columns instead of the declared track list: the row-anchor (.df-grid.card, spanning the full row for zebra/border/selection styling) occupied every column of its row for auto-placement purposes, leaving no free cell for a real cell to land in, and the hidden header-measurement clone competed with whichever record was first in the mounted window for the same reason. The row-anchor is nowposition: absolute(still filling its grid area viainset: 0), removing it from auto-placement bookkeeping entirely; the clone gets its own permanently-reserved rows instead of sharing one with a real record, collapsed to zero height/padding/border so the reservation stays invisible. - The row-anchor (
.df-grid.card) intercepted clicks meant for cell content underneath it: beingposition: absolute, it is a stacking-context participant painted above its static in-flow siblings by default, and nothing pulled it back behind them. The body grid now setsisolation: isolateand the row-anchorz-index: -1, scoped to that grid's own children so it doesn't affect stacking decisions further up the page. windowing.recompute()could mount the wrong end of the dataset on first render: called before the body grid has a real measured height, it read that as "scrolled past every record" and clamped the window to the lastbufferrecords instead of the first ones a fresh mount actually needs, visible as the grid appearing to load already scrolled to the bottom until the user scrolled or resized. It now mounts abuffer-sized window from the current scroll position (usually still0this early) instead.- The header/filter row's cached height (
min-height, used so it isn't left atautoand resized by every scroll-driven reflow) could lock in a value taller than the header actually needs and never correct itself: nothing re-measured it once the body grid's real column widths arrived later, and a measurement taken while a filter-row input was still mid-layout could read too tall in the first place. It now re-measures when column widths change and retries across a few animation frames until two consecutive readings agree. - The hidden header-measurement clone's reserved rows showed as a visible blank gap above the first real row, growing with how many rows a layout stacks per record (barely noticeable for a single-row layout, over a centimetre for one stacking many fields into a single column): the clone's own height collapses to zero, but the consumer's own
row-gapstill applied between each reserved track and before the first real row. The body scroller is now shifted up and grown by that same measured amount, clipping the reservation away without losing any scrollable height at the bottom. - The summary bar (loading, no-data, or a consumer's own
showSummaryBarcontent) could render entirely outside the visible, clipped area:.df-grid-body's scroller washeight: 100%, claiming the whole of.df-grid-body's own box in plain block flow and leaving the summary bar — its sibling — no room to render in, invisible in every state that shows it rather than just a layout tight on space..df-grid-bodyis now a flex column so the two share its height properly, the scroller shrinking to make room instead of always claiming all of it. With no records at all, the bar also now renders where a row would — right below the header — instead of at the bottom of the empty scroller with nothing visually anchoring it there; loading a further page of an already-populated grid keeps its default position at the bottom, where the new rows are about to arrive.
Added
estimatedRowHeightprop: row height, in pixels, assumed for a not-yet-rendered record — used to size the placeholder standing in for windowed-out rows. Default30; pick something close to your actual row height, since the grid does not average measured heights to refine this for you.minRenderedRowsprop: minimum number of records kept mounted on each side of the visible range. Default100, replacing the removedmainShadowCount.rowsfield onResponsiveColumnDefinition: declares how many grid rows that layout's card occupies per record (default1), needed for the relative--row-basecell placement above.
Removed
mainShadowCountprop — the primary shadow grid it configured no longer exists; column widths are now resolved natively rather than measured on a hidden copy.
[0.4.1] - 2026-09-06
Added
<DfGrid>exposesreMeasure()on its template ref: re-measures column widths off the shadow grid and copies them onto the container, for layout changes the grid cannot detect on its own - for example a column's rendered content changing width without the container itself resizing. Returns a promise that resolves once the new widths have actually reached the container.
Fixed
- The shadow grid's column-width measurement no longer briefly reads
grid-template-columnsas"none"and copies that onto the container, collapsing the row layout for a frame. That value is what the property reads back as in the gap between the shadow grid's element existing in the DOM and the stylesheet rule that makes it a grid taking effect; it is now retried on the next animation frame instead.
[0.4.0] - 2026-09-01
Added
- Every user-facing string the grid draws itself is translatable through
translatableStrings/translateStrings, from@dynamicforms/translatable: the loading and no-data indicators, the filter row's default placeholder, the selection status bar's cancel/invert tooltips and item count, the active-filter count, and the placeholder text a cell with anullvalue renders. Reading a key inside a template or acomputedsubscribes to it, so a latertranslateStringscall reaches what's already on screen.
Changed
- Bumps the
@dynamicforms/vue-formspeer range to^1.0.0and@dynamicforms/vuetify-inputsto^0.11.0, and adds@dynamicforms/translatable(^0.1.0) as a peer dependency.
[0.3.3] - 2026-08-28
Fixed
- A column's sort indicator no longer disappears when its
sortableconfig setskey(ornulls,compare,locale) without an explicitdirection- e.g. a compound-key column such assortable: { key: ['width', 'height'] }. The header computed whether to draw the indicator from the rawsortable.directionfield instead ofgetSortConfig(), which defaultsdirectionto'both'for such columns; clicking the header already sorted correctly throughgetSortConfig, only the indicator was suppressed.
[0.3.2] - 2026-08-27
Fixed
- A column's header no longer draws sort-direction arrows when its
sortableisfalse.SortingIndicatorreaddirectionandindexbut neversortable, so every header drew the asc/desc chevrons regardless of whether clicking them did anything - indistinguishable from an actually-sortable, currently-unsorted column. - The sort indicator is toned down to
opacity: .55so it no longer reads as bolder than the column label it sits next to, and the header label no longer stretches to push the indicator to the cell's far edge - it now sits right next to the label, so neighbouring columns' labels and indicators don't run together across the gap between them.
[0.3.1] - 2026-08-22
Changed
- The peer range on
@dynamicforms/vuetify-inputsmoves to^0.10.0. That release rebuilds<df-rtf-editor>on TipTap instead of CKEditor 5, so the VitePress SSR plugin that stubbed out CKEditor's browser-only modules during pre-rendering (docs/.vitepress/ssr-ckeditor-stub.ts) has nothing left to stub and is removed along with its wiring indocs/.vitepress/config.ts.
[0.3.0] - 2026-08-22
Changed
- The peer ranges move to
@dynamicforms/vue-forms@^0.17.1and@dynamicforms/vuetify-inputs@^0.9.2, andvuerises to^3.5.2withengines.nodeat>=22. The last two are floors the peer libraries themselves impose: the type declarations vue-tsc emits forMessagesWidgettake aDefineComponentwith 20 type arguments, and that shape is only available from Vue 3.5.2. The docs workspace carries the same floors, plusvuetify@^3.9as stated by vuetify-inputs 0.9. - Filter state construction follows
Field.create()being removed from vue-forms 0.6.0:createFilterState()now builds fields withnew Field(). A field built withvalue: nullreads backnullinstead ofundefined, soGridFilterEvent.filterValuescarries anullentry for a filterable column with no filter set, rather than omitting the key. Local filtering and the active-filter count treat both the same way, so what the grid filters is unchanged - only the documented shape offilterValuesreflects thenull.
[0.2.0] - 2026-08-14
Added
- Statement coverage raised from 74.94% to 95% with new specs for
use-excessive-scroll.ts,use-recently-added.ts,incoming-arc.vue,header-renderers.tsand grid lifecycle behaviour (visible-range reporting, overflow learning, teardown), plus a dedicated auto-sizing spec and Playwright regression tests covering overlay and classic scrollbars.
Fixed
- The grid no longer overflows its container in a Vuetify app. Column widths are measured on a hidden shadow grid, absolutely positioned against
.df-grid.container; withoutposition: relativeon that container the shadow stretched to whatever ancestor happened to be positioned instead -.v-application__wrap, the full page width - and the widths measured there were copied onto rows that were only as wide as the actual container. - Header column widths account for the scrollbar gutter correctly on both overlay and classic scrollbars. The scroller declared
scrollbar-gutter: stable, but the reservation it gets does not match what the surrounding layout reserves: Chromium with overlay scrollbars still reserves roughly 15px for the gutter while the container around it reserves nothing, leaving the header short of the tracks it was given; Firefox reserves nothing for either. The scrollbar width is now measured directly off the scroller (offsetWidth - clientWidth), published as--df-grid-scrollbar-width, and consumed by the header as padding - re-measured on mount, on container resize, and whenever rows update, since incoming rows can make the scrollbar appear without a resize event. - A row click or long-press resolves against the displayed (sorted and filtered) record list instead of the raw
recordsprop. Rows carry their position in the displayed list asdata-idx, but the mouse-event handler looked that position up inrecords; under any active sort or filter this pointed at the wrong record, so a click or long-press could select a different row than the one the pointer landed on. - Selecting a row no longer forces every visible card to remount. Each column's render options carried a fresh
Symbolkeyed to the current selection mode, so toggling selection produced a new options object for every column on every card, even ones unaffected by the change.
[0.1.8] - 2026-08-13
Added
typesandmoduleexport conditions on the package manifest. Withouttypes, TypeScript could not resolvedist/index.d.tsthrough the.export, so consumers got implicitanyfor the whole grid;modulepoints bundlers that still read it at the ESM build instead of the UMD one.- A GitHub Actions CI workflow: lint, test, build and docs build on two Node versions, incremental coverage reporting on pull requests, and a second job running the Playwright e2e suite against the docs dev server.
[0.1.7] - 2026-08-01
Added
- Column sorting: clicking a header cycles a column through ascending, descending and unsorted (skipping descending where a column disallows it), multiple columns can be sorted at once, and sort state can be used either as an uncontrolled internal state or as a
v-model:sortStatebound by the consumer. Asortevent reports the clicked column and the suggested resulting sort state. - Column filtering: any column can be marked
filterable(a boolean for a plain string filter, or aFilterConfigchoosing the field type -string,number,booleanordate- a list of choices, or a placeholder). Filter state is a vue-formsGroupofFields keyed by column name. A filter can be marked to be resolved on the backend instead of locally, in which case the grid emits the filter state for the consumer to act on rather than filtering the displayed rows itself. - Row selection via
useSelection, with four modes:null(selection inactive),'selection'(selectionKeysholds the selected keys),'exclusion'(selectionKeysholds the keys excluded from an otherwise all-selected set), and'non-select'(selection disabled). Selection works both controlled, throughselectionMode/selectionKeysprops and theirupdate:*events, and uncontrolled, with the grid managing its own state. - Toolbar and footer slots (
toolbar-start,toolbar-endabove the header;footer-start,footer-endbelow the scroller) for placing custom content around the grid. - Pre- and post-render hooks on a column's renderer options (
preRender,postRender) for wrapping a cell's rendered value with additional content, such as an icon or action button, without replacing the renderer itself. - A summary bar below the data rows, shown automatically when
loadingistrueorrecordsis empty, and configurable to show at other times. Whileloadingistruethe bar shows a spinner and the no-data state is suppressed even whenrecordsis empty. - A
loadevent (direction: 'vertical' | 'horizontal'), proxied from the underlying virtual-scrollloadevent and fired when the user scrolls withinloadDistancepx (default 200) of the end of the list whileloadingisfalse. Use it to fetch and append the next page of records; settingloadingtotruewhile a fetch is in flight suppresses duplicate events until it completes. - An overscroll indicator and
excessive-scrollevent: scrolling past either end of the list accumulates a visible overscroll displacement (up to 60px), and whenexcessiveScrollThresholdis set, crossing that percentage of the maximum fires anexcessive-scrollevent with the signed displacement - positive past the bottom, negative past the top. The event fires at most once per crossing, gated by a 1-second minimum interval and requiring the displacement to fall back under the threshold first. The visual indicator itself is always shown regardless of whether the event is wired up. - Pull-to-refresh, built on the same overscroll tracking used for the excessive-scroll indicator.
- An incoming-records indicator (
recentlyAdded, viauseRecentlyAdded): rows added to the data set flash, and an arc overlay flashes at the top or bottom edge of the viewport when a newly added row is currently scrolled out of view. The grid callssetVisibleRange()on the composable automatically as the viewport scrolls;incomingArcMaxOpacitycontrols the peak opacity of the arc, with rapid repeated flashes decaying toward a lower opacity automatically.
Changed
- The sort indicator direction is inverted: an ascending sort now shows the indicator that previously meant descending, since watching a descending-looking indicator while data was actually sorted ascending read as backwards.
- Column render options no longer collide across grid instances: each column's options are stamped with the owning grid's id and column identity internally.
Fixed
- The published CSS path in the package manifest now points at
dist/dynamicforms-vue-grid.css, matching the file the build actually emits, instead of the staledist/style.css. - Row selection survives a reflow of the underlying data (sort, filter, or the record list changing shape) without losing or misattributing which rows are selected.
- Column width measurement stays correct when selection mode is switched on or off, rather than leaving the shadow grid's measurements out of sync with the visible one.
[0.1.6] - 2025-09-07
Added
- Mouse handling on the grid:
clickanddblclickevents report the row index, key, row data and the CSS classes of the clicked column. - A dedicated header component (
df-grid-header.vue), with grid layout, mouse-event handling and per-column measurement logic factored out of the main grid component into their own modules. - Dynamic column configuration: columns are declared as data (field name, renderer, renderer options, CSS class) via
createColumn()rather than being derived implicitly from the data shape, and header cells render from the same column definitions. - Column width is now measured against the header row as well as the data rows, so header content that is wider than any cell in a column no longer gets clipped.
Changed
- Responsive layout rendering is optimised to avoid redundant measurement passes.
[0.1.4] - 2025-09-07
Fixed
- The shadow grid used for measuring column widths renders with the same markup as the visible grid again, after drifting out of sync with it.
[0.1.3] - 2025-09-06
Added
- A cell renderer system: a
CellRendererTransformerfunction per data type (plain,md,color,checkbox,link,email,file,ip4,ip6,ip,date,time,datetime,int,float,decimal), registered in aDefaultRenderersmap that a consumer can extend viasetCellRenderer(). createColumn()andColumnDefinitionfor declaring a column's field name, renderer, renderer options and CSS class.
Changed
- The card-based row rendering (
df-table.vue) is replaced bydf-grid.vue, rendering rows through the new cell renderer system instead of dumping raw field values.
[0.1.0] - 2025-08-30
Added
- Initial release: a virtual-scrolling data grid for Vue 3, built on
vue-virtual-scroller, rendering arbitrary records as user-supplied row cards with a shadow grid used to measure column widths for CSS grid layout.
