Guide
Connecting to a backend
Every component in the kit that renders a collection speaks the same four types, so wiring a table, a kanban board and a notification feed to your API is one shape of work, not three. This page is that shape — and it ends with a table you can break on purpose.
The contract
Four types from @styloviz/core. No HTTP, no rxjs, no opinion about where your data comes from — a component that imported HttpClient would be unusable from tRPC, GraphQL or a WebSocket, which is most of the reason this layer exists.
- SvDataQuery
- What the user is asking for: page, page size, sorts, filters, search, cursor. Emitted as one object, so you write one handler however they changed it.
- SvPage<T>
- What came back:
items,totalandcursor. Anulltotal means the server did not say — which is not the same as zero. - SvDataError
- What went wrong, already safe to render.
toSvDataError()derivesretryablefrom the status, so a 404 never gets a button that can only fail again. - SvItemKey<T>
- How a row keeps its identity across refreshes. Tracking by index means a reorder moves focus to the wrong element and shows the wrong row as selected.
The short version
@styloviz/data is optional sugar — no component depends on it. What it saves you is the four things a hand-rolled subscribe tends to forget.
Install
npm i @styloviz/dataA server-backed table
The component knows nothing about HTTP; the controller knows nothing about tables.
import { Component } from '@angular/core'; import { SvDataTableComponent } from '@styloviz/data-table'; import { svDataController, createHttpDataSource } from '@styloviz/data'; @Component({ selector: 'app-invoices', imports: [SvDataTableComponent], template: ` <sv-data-table [serverSide]="true" [columns]="columns" [rows]="data.items()" [loading]="data.loading()" [error]="data.error()" [totalCount]="data.total()" (queryChange)="data.setQuery($event)" (retry)="data.reload()" /> `, }) export class InvoicesComponent { readonly columns = [ { key: 'customer', label: 'Customer', sortable: true }, { key: 'amount', label: 'Amount', sortable: true }, ]; readonly data = svDataController( createHttpDataSource<Invoice>({ url: '/api/invoices' }), ); }
- 1Debounce. Typing five characters is one request. A page or sort click is not debounced at all — that is a deliberate act, not a stream of keystrokes, and waiting 300 ms after it just feels broken.
- 2A request guard. The user types ab, then abc. Two requests go out; ab is slower and lands second. Without a guard the list ends up showing results for a query the user has already moved past — and nothing in the code looks wrong.
- 3Real cancellation. Ignoring a superseded response is not enough: the request keeps running and holds a connection. On a slow link that is exactly what makes filtering feel laggy.
- 4Retry that knows when not to. Network failures, 429 and 5xx are retried with jittered backoff. A 404 or a 422 is surfaced immediately — retrying those only delays telling the user something they need to act on.
Matching your API
Three presets cover most REST layers. None of them asks you to change the server.
Query shape
import { offsetLimitQuery, pageNumberQuery, cursorQuery } from '@styloviz/data'; // ?offset=40&limit=20 — the default, and what SQL LIMIT/OFFSET maps to. createHttpDataSource({ url: '/api/invoices', mapQuery: offsetLimitQuery() }); // ?page=3&perPage=20 — 1-based, as Rails, Laravel and JSON:API emit. createHttpDataSource({ url: '/api/invoices', mapQuery: pageNumberQuery() }); // ?cursor=abc&limit=20 — keyset. The cursor is omitted on the first page, // and dropped automatically whenever the sort or filters change: a cursor is // only meaningful against the ordering that produced it. createHttpDataSource({ url: '/api/invoices', mapQuery: cursorQuery() }); // Anything else is a pure function of the query. const mapQuery = (q) => ({ skip: String((q.page - 1) * q.pageSize), take: String(q.pageSize) });
Response shape
// Recognised without configuration: a bare array, or an object with the rows // under items / data / results / records / rows, and the count under // total / totalCount / count / meta.total. createHttpDataSource({ url: '/api/invoices' }); // Anything else — one function, no server changes. createHttpDataSource({ url: '/api/invoices', mapResponse: (body) => ({ items: body.payload.edges.map((e) => e.node), total: body.payload.totalCount, cursor: body.payload.pageInfo.endCursor ?? null, }), });
Without the adapter package
The contract is three inputs and two outputs. If you already have a data layer you like, keep it — you are not giving anything up except the four items above, which you can also write yourself.
Plain signals
// No adapter package required — the contract is just three inputs. readonly rows = signal<Invoice[]>([]); readonly loading = signal(false); readonly error = signal<SvDataError | null>(null); async load(query: SvDataQuery): Promise<void> { this.loading.set(true); this.error.set(null); try { this.rows.set(await this.api.invoices(query)); } catch (e) { // Derives `retryable` from the status, so a 404 gets no "Try again". this.error.set(toSvDataError(e)); } finally { this.loading.set(false); } }
Live feeds
Streaming components take events one at a time rather than a rebuilt array per token, and they take a connection state they cannot infer: a list that stops growing looks identical whether the agent is thinking or the socket died ten minutes ago. Users resolve that ambiguity by reloading — which on a stream loses everything that already arrived.
An EventSource into an activity log
// A live feed appends; it does not hand you a new array per token. @ViewChild(SvAiAgentActivityLogComponent) log!: SvAiAgentActivityLogComponent; private open(): void { const es = new EventSource('/api/runs/42/events'); es.onopen = () => this.connection.set('open'); es.onerror = () => this.connection.set('reconnecting'); es.onmessage = (m) => this.log.appendEvent(JSON.parse(m.data)); }
Break it on purpose
A real <sv-data-table> over 84 rows, behind an artificial network. Add latency and watch the skeleton, the debounce and the request guard become visible; force a failure and watch what survives it.
Invoices
84| Invoice | Customer | Status | Amount |
|---|---|---|---|
| 1000 | Northwind Traders | paid | 120 |
| 1001 | Acme Logistics | open | 157 |
| 1002 | Blue Harbour Ltd | overdue | 194 |
| 1003 | Contoso Retail | paid | 231 |
| 1004 | Fabrikam Devices | open | 268 |
| 1005 | Globex Media | overdue | 305 |
| 1006 | Initech Systems | paid | 342 |
| 1007 | Umbra Analytics | open | 379 |
| 1008 | Northwind Traders | overdue | 416 |
| 1009 | Acme Logistics | paid | 453 |
Showing 1 – 8 of 84
How the demo fakes a network
The same decorators are exported for your own tests.
import { createArrayDataSource, withLatency, withFailure } from '@styloviz/data'; // The states that never appear against a local array — the skeleton, the // stale-while-loading table, the superseded request — made reachable. readonly source = withFailure( withLatency(createArrayDataSource(INVOICES), () => this.latencyMs()), { when: () => this.broken(), status: 503 }, );
Things worth trying: search while latency is high and watch the rows stay put rather than blanking; turn on failure mid-search and note that the toolbar and the rows survive, so you can undo the filter that caused it; switch 503 to 404 and watch the retry button disappear.