@iyulab/flex-table 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.54.0] - 2026-10-07
4
+
5
+ ### Added
6
+
7
+ - **`error`** property — the last load failure (`{ message }`, so a data source's `SourceError` goes in as is). While
8
+ set, the grid shows `error.message` as an alert where the rows or the empty state would be; a failed query used to
9
+ look like "No data" unless the app drew its own message beside the grid.
10
+
3
11
  ## [0.53.0] - 2026-10-07
4
12
 
5
13
  ### Added
package/README.md CHANGED
@@ -113,6 +113,7 @@ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both s
113
113
  | `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Footer/summary row data (keys match column keys) |
114
114
  | `emptyMessage` | `empty-message` | `string` | `'No data'` | Shown when `data` is empty |
115
115
  | `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | Shown when `data` has rows but every one is hidden by an active column filter |
116
+ | `error` | — | `{ message: string } \| null` | `null` | The last load failure. While set, `error.message` is shown as an alert where the rows or the empty state would be, so a failed query does not look like "no data". Pass a data source's `error` as is |
116
117
  | `stylesheets` | — | `CSSStyleSheet[]` | `[]` | Constructable stylesheets adopted into the shadow root alongside the grid's own styles — the escape hatch for styling content a `render` function inserts, since document CSS doesn't cross the shadow boundary. Reassigning swaps the previous set, it doesn't accumulate |
117
118
 
118
119
  ### Read-only Properties
@@ -816,7 +817,7 @@ The hook returns:
816
817
  |---|---|
817
818
  | `data` / `totalCount` | Current page rows and the server's total (`@odata.count`). When the server pages its response (`@odata.nextLink`, e.g. a page size smaller than `pageSize`), the hook follows the link until the page is filled; a link outside the request's origin, or one that returns to a page already read, is reported through `error` instead of showing a short page |
818
819
  | `loading` | A request is in flight, or none has answered yet (true from the first render until the first response settles, and while `enabled: false`) |
819
- | `error` | The last failed request, or `null`: `{ message, status?, code?, details?, body? }`. **Render `error.message`** — a failed request otherwise leaves the grid silently empty. Branch on `status` (HTTP), `code` (the server's rejection code, OData `error.code`) and `details` (OData `error.details`); `body` is the parsed response (or its text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has a `message` only |
820
+ | `error` | The last failed request, or `null`: `{ message, status?, code?, details?, body? }`. **Pass it to the table** (`error={source.error}`) or render `error.message` yourself — a failed request otherwise leaves the grid silently empty. Branch on `status` (HTTP), `code` (the server's rejection code, OData `error.code`) and `details` (OData `error.details`); `body` is the parsed response (or its text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has a `message` only |
820
821
  | `page` / `setPage` | Zero-based page index |
821
822
  | `sortCriteria` / `onSortChange` | Bind `onSortChange` to the table's `sort-change` event |
822
823
  | `search` / `setSearch` | Current search term and its setter (resets to page 0) |
@@ -888,8 +889,7 @@ class OrdersPage extends LitElement {
888
889
  render() {
889
890
  const { data, loading, error } = this.orders.state;
890
891
  return html`
891
- ${error ? html`<p role="alert">${error.message}</p>` : ''}
892
- <flex-table data-mode="server" .data=${data} .loading=${loading}
892
+ <flex-table data-mode="server" .data=${data} .loading=${loading} .error=${error}
893
893
  @sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
894
894
  }
895
895
  }
@@ -392,6 +392,10 @@ var S = i`
392
392
  color: var(--ft-empty-color);
393
393
  }
394
394
 
395
+ .ft-error {
396
+ color: var(--ft-invalid-color);
397
+ }
398
+
395
399
  .ft-has-comment {
396
400
  position: relative;
397
401
  }
@@ -1752,7 +1756,7 @@ var K = {
1752
1756
  lte: "≤"
1753
1757
  }, q = 120, J = 40, Y = 20, X = 32, Z = 5, Xe = 200, Q = class extends r {
1754
1758
  constructor(...e) {
1755
- super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._focusedCellEl = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new k(), this._undo = new A(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
1759
+ super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.error = null, this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._focusedCellEl = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new k(), this._undo = new A(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
1756
1760
  e.composedPath()[0] !== this || this._activeCell || this._headerCol !== null || this.visibleColumns.length !== 0 && this.matches(":focus-visible") && (this._visibleRowCount > 0 ? (this._selection.setActive(0, 0), this._syncActiveCell()) : this._headerCol = 0);
1757
1761
  }, this._onFocusIn = (e) => {
1758
1762
  let t = e.composedPath()[0];
@@ -4153,8 +4157,8 @@ var K = {
4153
4157
  for (let e of u) h.push(this._renderHeaderCell(t[e], e));
4154
4158
  for (let e = c; e < l; e++) h.push(this._renderHeaderCell(t[e], e));
4155
4159
  let g = this._colDragIndicatorLeft == null ? "" : a`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
4156
- if (this.data.length === 0 || this._visibleRowCount === 0) {
4157
- let e = this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage;
4160
+ if (this.error || this.data.length === 0 || this._visibleRowCount === 0) {
4161
+ let e = this.error ? a`<div class="ft-empty ft-error" role="alert">${this.error.message}</div>` : a`<div class="ft-empty">${this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage}</div>`;
4158
4162
  return a`
4159
4163
  ${r}
4160
4164
  <div class="ft-header" role="row" aria-rowindex="1" style="width: ${s}px; height: ${i}px;">
@@ -4162,7 +4166,7 @@ var K = {
4162
4166
  ${h}
4163
4167
  ${g}
4164
4168
  </div>
4165
- <div class="ft-empty">${e}</div>
4169
+ ${e}
4166
4170
  `;
4167
4171
  }
4168
4172
  let { start: _, end: v } = this.visibleRange, y = this._frozenRowCount, b = [];
@@ -4449,7 +4453,7 @@ G([c({ type: Array })], Q.prototype, "columns", void 0), G([c({
4449
4453
  })], Q.prototype, "emptyMessage", void 0), G([c({
4450
4454
  type: String,
4451
4455
  attribute: "no-matching-message"
4452
- })], Q.prototype, "noMatchingMessage", void 0), G([c({
4456
+ })], Q.prototype, "noMatchingMessage", void 0), G([c({ attribute: !1 })], Q.prototype, "error", void 0), G([c({
4453
4457
  type: Boolean,
4454
4458
  reflect: !0
4455
4459
  })], Q.prototype, "loading", void 0), G([c({
@@ -72,6 +72,14 @@ export declare class FlexTable extends LitElement {
72
72
  * column filters (0 visible rows, non-empty `data`). Default: 'No matching data'.
73
73
  */
74
74
  noMatchingMessage: string;
75
+ /**
76
+ * The last load failure, or `null`. While set, the grid shows `error.message` in an alert where rows or the empty
77
+ * state would be — a failed query otherwise looks like "no data". Takes a data source's `error` as is
78
+ * (`useODataSource`/`createODataSource`, a `SourceError`): any `{ message }`.
79
+ */
80
+ error: {
81
+ message: string;
82
+ } | null;
75
83
  /**
76
84
  * True일 때 그리드 위에 로딩 오버레이를 표시하고 host에 `aria-busy="true"`를 반영한다.
77
85
  * `useODataSource()`가 반환하는 `loading`과 자연 연동하도록 설계됨:
@@ -1,3 +1,3 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-DUAMhXCS.js";
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-DlvIBu4z.js";
2
2
  import { t as o } from "./locale-B1SiVDS1.js";
3
3
  export { a as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, o as flexTableLocale, r as renderCell };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-DUAMhXCS.js";
1
+ import { t as e } from "./flex-table-DlvIBu4z.js";
2
2
  import { a as t, t as n } from "./source-CwbD-6ea.js";
3
3
  import { t as r } from "./view-DiLr-TdY.js";
4
4
  import i, { useCallback as a, useEffect as o, useMemo as s, useState as c, useSyncExternalStore as l } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -126,7 +126,7 @@ Server paging from OData or an in-memory array uses the same binding — see `re
126
126
  - `--ft-row-height` is read once at first render; change row height later via `rowHeight` / `row-height`.
127
127
  - Styles for elements returned by `render` must be passed through the `stylesheets` property
128
128
  (constructable `CSSStyleSheet[]`); document CSS does not cross the shadow boundary.
129
- - Always render `error.message` from `useODataSource`; a failed request otherwise leaves the grid empty. Branch on `error.status` / `error.code` rather than parsing the message.
129
+ - Always show `error` from `useODataSource` — pass it to the table (`error={source.error}`); a failed request otherwise leaves the grid empty. Branch on `error.status` / `error.code` rather than parsing the message.
130
130
 
131
131
  ## References
132
132
 
@@ -23,6 +23,7 @@ All names below are members of the `FlexTable` class (`<flex-table>`).
23
23
  | `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Summary row keyed by column key |
24
24
  | `emptyMessage` | `empty-message` | `string` | `'No data'` | |
25
25
  | `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | All rows hidden by filters |
26
+ | `error` | — | `{ message: string } \| null` | `null` | Load failure shown as an alert instead of rows / empty state (a source's `error` as is) |
26
27
  | `loading` | `loading` | `boolean` | `false` | Loading overlay + `aria-busy` |
27
28
  | `importEnabled` | `import-enabled` | `boolean` | `false` | Drag-and-drop `.xlsx` / `.csv` import |
28
29
  | `clearUndoOnDataChange` | `clear-undo-on-data-change` | `boolean` | `false` | Replacing `data` clears undo history |
@@ -71,12 +71,12 @@ function Orders() {
71
71
  return (
72
72
  <>
73
73
  <input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
74
- {source.error && <p role="alert">{source.error.message}</p>}
75
74
  <FlexTableReact<Order>
76
75
  dataMode="server"
77
76
  columns={columns}
78
77
  data={source.data}
79
78
  loading={source.loading}
79
+ error={source.error}
80
80
  onSortChange={source.onSortChange}
81
81
  clearSelectionOnDataChange
82
82
  />
@@ -109,7 +109,7 @@ function Orders() {
109
109
  |---|---|
110
110
  | `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
111
111
  | `loading` | Request in flight, or no answer yet (true from the first render until the first response settles, and while `enabled: false`) — so `!loading && totalCount === 0` means "no results" |
112
- | `error` | `SourceError \| null` — `{ message, status?, code?, details?, body?, cause? }`; render `error.message`, branch on `status` / `code` (OData `error.code`). `message` is the server's sentence when it sent one; otherwise this package's, in the `flexTableLocale` language — `requestFailed` (`Request failed ({status})`) when a response came back, `networkFailed` when none did (the transport's exception is in `cause`) |
112
+ | `error` | `SourceError \| null` — `{ message, status?, code?, details?, body?, cause? }`; pass it to the table (`error`) or render `error.message`; branch on `status` / `code` (OData `error.code`). `message` is the server's sentence when it sent one; otherwise this package's, in the `flexTableLocale` language — `requestFailed` (`Request failed ({status})`) when a response came back, `networkFailed` when none did (the transport's exception is in `cause`) |
113
113
  | `page`, `setPage` | Zero-based |
114
114
  | `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
115
115
  | `search`, `setSearch` | Literal text (resets to page 0) |
@@ -170,8 +170,7 @@ class OrdersPage extends LitElement {
170
170
  private orders = new ODataSourceController<Order>(this, '/api/orders', { pageSize: 20 });
171
171
  render() {
172
172
  const { data, loading, error } = this.orders.state;
173
- return html`${error ? html`<p role="alert">${error.message}</p>` : ''}
174
- <flex-table data-mode="server" .data=${data} .loading=${loading}
173
+ return html`<flex-table data-mode="server" .data=${data} .loading=${loading} .error=${error}
175
174
  @sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
176
175
  }
177
176
  }