@iyulab/flex-table 0.52.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,31 @@
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
+
11
+ ## [0.53.0] - 2026-10-07
12
+
13
+ ### Added
14
+
15
+ - **`createODataSource(url, options)`** (`@iyulab/flex-table/odata`) — the OData source without React. It is what
16
+ `useODataSource` now runs on: the same options and state, the request, `@odata.nextLink` following, the page
17
+ fallback, the `fixedFilter` reset, `enabled`, cancellation and the structured `error`. `getState()` · `subscribe()` ·
18
+ `setPage` · `setSort` · `setSearch` · `refresh` · `update(url, options)`. Requests go out while someone is subscribed,
19
+ and changes in the same tick become one request.
20
+ - **`ODataSourceController`** — a Lit reactive controller over that source: subscribes when the element connects,
21
+ cancels when it disconnects, re-renders it on every change. A custom-element list page gets the same server paging
22
+ a React page gets from the hook.
23
+
24
+ ### Changed
25
+
26
+ - `useODataSource` reads `fetcher` and `onUnauthorized` when each request starts. A fresh function on every render no
27
+ longer needs `useCallback`; before, the one captured by the last request kept being used.
28
+
3
29
  ## [0.52.0] - 2026-10-07
4
30
 
5
31
  ### Changed
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
@@ -758,7 +759,7 @@ const source = useODataSource('/api/orders', {
758
759
  | `onUnauthorized` | — | Called on `401` responses, before the generic error is set. A `403` (signed in, not permitted) does not call it — it surfaces as `error` |
759
760
  | `enabled` | `true` | While `false`, no request is made and `loading` stays `true` — see below |
760
761
 
761
- `fetcher`/`onUnauthorized` should be stable references (e.g. wrap in `useCallback`) — they are intentionally excluded from the hook's internal effect dependencies to avoid refetch loops on every render.
762
+ `fetcher`/`onUnauthorized` are read when each request starts, so a fresh function on every render is fine — it neither refetches nor is ignored.
762
763
 
763
764
  Changing `fixedFilter` resets the page to `0`, the same way `setSearch` and `onSortChange`
764
765
  already do. All three change the size of the result set, so keeping the old `$skip` would
@@ -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) |
@@ -828,7 +829,7 @@ The hook returns:
828
829
 
829
830
  Terms are always quoted because OData 4.0 only allows letters in an unquoted `searchWord`, so `2026` or `ZT-E2E-A` would be rejected by servers that follow it (4.01 relaxed this, but [Microsoft.OData still lexes as 4.0](https://github.com/OData/odata.net/issues/2445)). Quoting keeps any term valid regardless of server version. Since a `$search` phrase cannot contain `"` and OData defines no escape for it, double quotes are stripped from the term.
830
831
 
831
- The quoting/escaping logic above is also available standalone as `buildSearchExpression(term)`, for consumers that need the same `$search` encoding without the pagination hook (e.g. a typeahead/combobox that isn't a table). `parseOrderBy(orderBy)` (`'a asc, b desc'` → `SortCriteria[]`) is exported the same way, for consumers driving a sort UI that isn't `useODataSource` either. The `./odata` entry holds only pure functions and does not load React, so an app without React can use it (the hooks live on `./react`):
832
+ The quoting/escaping logic above is also available standalone as `buildSearchExpression(term)`, for consumers that need the same `$search` encoding without the pagination hook (e.g. a typeahead/combobox that isn't a table). `parseOrderBy(orderBy)` (`'a asc, b desc'` → `SortCriteria[]`) is exported the same way, for consumers driving a sort UI that isn't `useODataSource` either. The `./odata` entry does not load React, so an app without React can use it (the hooks live on `./react`):
832
833
 
833
834
  ```ts
834
835
  import { buildSearchExpression, parseOrderBy } from '@iyulab/flex-table/odata';
@@ -852,6 +853,48 @@ buildODataQuery({
852
853
  // '?$filter=IsActive eq true&$orderby=name desc&$count=true&$top=20&$skip=40&$search=%22red%22%20AND%20%22shirt%22'
853
854
  ```
854
855
 
856
+ ### OData Source without React
857
+
858
+ `useODataSource` is a thin adapter over a framework-neutral source, and that source is public: `createODataSource(url, options)` takes the same options and does everything the hook does — the request, `@odata.nextLink` following, page fallback, `fixedFilter` reset, `enabled`, cancellation and the structured `error`. Use it from a Lit element, another framework, or plain code.
859
+
860
+ ```ts
861
+ import { createODataSource } from '@iyulab/flex-table/odata';
862
+
863
+ const orders = createODataSource<Order>('/api/orders', { pageSize: 20 });
864
+ const off = orders.subscribe(() => render(orders.getState())); // the first subscriber starts loading
865
+ orders.setSort([{ key: 'name', direction: 'asc' }]); // also: setPage, setSearch, refresh
866
+ orders.update('/api/orders', { pageSize: 20, fixedFilter: { IsActive: true } }); // changed options
867
+ off(); // the last unsubscribe cancels a request in flight
868
+ ```
869
+
870
+ | Member | Description |
871
+ |---|---|
872
+ | `getState()` | `{ data, totalCount, loading, error, page, sortCriteria, search }` — the same fields the hook returns. A new object only when something changed |
873
+ | `subscribe(listener)` | Called on every change; returns the unsubscribe function. Requests go out only while someone is subscribed |
874
+ | `setPage(page)` | Zero-based |
875
+ | `setSort(criteria)` / `setSearch(term)` | Change the sort or search and go back to page 0 |
876
+ | `refresh()` | Re-read with the same conditions (nothing while `enabled: false`) |
877
+ | `update(url, options)` | New options. Only a changed request re-reads; a changed `fixedFilter` value goes back to page 0; `initial*` are read at creation only |
878
+
879
+ Changes made in the same tick become one request with the final conditions, so `setSearch` followed by `setPage` does not send the intermediate one.
880
+
881
+ For a Lit element, `ODataSourceController` ties a source to the element's lifecycle — it subscribes when the element connects, cancels when it disconnects, and re-renders it on every change:
882
+
883
+ ```ts
884
+ import { ODataSourceController } from '@iyulab/flex-table/odata';
885
+
886
+ class OrdersPage extends LitElement {
887
+ private orders = new ODataSourceController<Order>(this, '/api/orders', { pageSize: 20 });
888
+
889
+ render() {
890
+ const { data, loading, error } = this.orders.state;
891
+ return html`
892
+ <flex-table data-mode="server" .data=${data} .loading=${loading} .error=${error}
893
+ @sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
894
+ }
895
+ }
896
+ ```
897
+
855
898
  ### Array Source Hook (React)
856
899
 
857
900
  `useArraySource(data, options)` runs search/sort/pagination over an in-memory array and