Skip to content

re-architect table state per RFC #226 - #239

Open
F0rty-Tw0 wants to merge 10 commits into
mainfrom
art/feat/refactor-state
Open

F0rty-Tw0 wants to merge 10 commits into
mainfrom
art/feat/refactor-state

Conversation

@F0rty-Tw0

@F0rty-Tw0 F0rty-Tw0 commented Jul 3, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

  • De-bulk NatTableState: the 9 hand-synced controlled/internal signal pairs collapse onto a controlledSlice primitive, TanStack table creation moves to a table-instance factory, and intl + a11y-text derivation split into a leaf NatTableIntlService and the existing NatTableA11yService. The store drops from 635 to ~542 code lines; its max-lines disable is rewritten (kept, scoped) to name the genuinely irreducible residual.
  • Complete typed-command facade on NatTableUiController: patchState(updater) is replaced with setGlobalFilter, setColumnFilter, setColumnSort, setColumnVisible, setRowSelected, clearRowSelection, setPageSize, goToPage, nextPage, previousPage. Commands are absolute and safe: unknown column/row ids no-op, nextPage guards on canNextPage, and goToPage clamps to the valid page range (TanStack itself only clamps the upper bound in manual pagination).
  • Typed read selectors: pagination, pageCount, canPreviousPage, canNextPage, globalFilter, columnFilters, sorting, rowSelection, and columnVisibility (per-column { id, label, visible, canHide } items, labels resolved the same way the built-in controls resolve them).
  • The a11y service no longer writes state.resizeCommit. That clear moves to resize start, so a11y only reads state.

Impact on existing code

  • Breaking: patchState is removed from the controller contract. Companion controls and any consumer that drove state through updater closures now call the typed commands. NatTableService.patchState(config), the surface's config method, is a different API and is unchanged.
  • controller.table is @deprecated and every previous use case now has a typed alternative. The column-visibility control runs entirely on columnVisibility() + setColumnVisible(). The one deliberate exception is the export directive: its handler context exposes the raw TanStack table by contract, so it keeps a single documented .table read.
  • Migrated here: the pagination, page-size, pager, search, render-metrics filter, and column-visibility controls, plus the consumer-owned search doc example. A dead .table computed in the showcase search component is removed.
  • Docs now steer programmatic sorting to setColumnSort (sorting, responsive-capabilities, header-actions jsdoc, best-practise skills); the raw table.setSorting(...) path stays documented for multi-column sorts only.
  • The controlled/uncontrolled per-slice state feature is untouched. RFC Refactor (RFC): re-architect NatTable state to one source of truth, a typed-command facade, and one-way flow #226's "one source of truth" is intentionally not done, since it would delete that feature (several demos rely on it).

Test plan

  • New controller specs: table.pagination-controller.spec.ts (13 tests: pageCount edges incl. pagination-disabled and manual mode, navigation guards, atomic setPageSize, filter-driven first-page resets) and table.commands-controller.spec.ts (11 tests: sort set/replace/remove, visibility toggle + canHide guard, selection set/clear + disabled guard, unknown-id no-ops).
  • Resize a column with the pointer and confirm the live region announces the final committed width.
  • Exercise the pagination, page-size, search, and column-visibility controls; they read the new selectors and dispatch the typed commands.
  • Full gates on the final tree: core 594 tests, all 5 projects, publish build, lint.

Closes #226

@vercel

vercel Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
angular-advanced-table Ready Ready Preview, Comment Jul 10, 2026 7:17pm

@F0rty-Tw0
F0rty-Tw0 requested a review from VaggelisKa July 3, 2026 17:12
@greptile-apps

greptile-apps Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — typed commands and selectors are thoroughly guarded, the controlledSlice primitive is well-tested, and all companion controls are migrated consistently.

All nine state slices are correctly wired through controlledSlice with appropriate read/write normalization hooks. Every command guards against unknown column/row ids via optional chaining or explicit checks. The nextPage/previousPage asymmetry is intentional and correct: TanStack's client-side nextPage is an unclamped increment (requiring the facade guard), while previousPage uses Math.max(0, old-1) internally (no guard needed). The new specs cover all documented edge cases. No regressions found in the companion control migrations.

No files require special attention.

Important Files Changed

Filename Overview
libs/ng-advanced-table/src/utils/controlled-slice.util.ts New utility that encapsulates the controlled/internal signal pattern with optional read/write normalization hooks; well-tested with 7 unit specs covering all branches.
libs/ng-advanced-table/src/table/table.ts Implements all 10 typed commands and 9 typed selectors on NatTableUiController; nextPage guards on canNextPage(), previousPage delegates to TanStack which clamps at 0 internally, and all column/row commands no-op on unknown ids via optional chaining or explicit checks.
libs/ng-advanced-table/src/domain-logic/table.state.ts Replaces 9 hand-synced controlled/internal signal pairs with controlledSlice instances and delegates TanStack construction to table-instance.factory and intl to NatTableIntlService; updateState correctly re-resolves all 9 slices on every call (consistent with prior behavior).
libs/ng-advanced-table/src/domain-logic/table-instance.factory.ts Extracts TanStack table creation from NatTableState; callbacks correctly pass firstPageUpdater on globalFilter/columnFilter changes and delegate all state mutations back through store.updateState.
libs/ng-advanced-table/src/domain-logic/table-intl.service.ts New leaf service that resolves locale/intl/a11y-text, injecting only NatTableService and NAT_TABLE_INTL to avoid DI cycles; cleanly mirrors what was in NatTableState before.
libs/ng-advanced-table/src/common/ui-controller.type.ts Replaces single generic patchState with 10 typed command methods and adds 9 typed signal selectors; table accessor deprecated with clear JSDoc, NatTableColumnVisibilityItem exported for consumers.
libs/ng-advanced-table/src/domain-logic/table-a11y.service.ts Moves ARIA id computeds and resolved a11y text computeds from NatTableState here; delegates intl to NatTableIntlService; resizeCommit is no longer cleared inside the effect (intentional, flagged in previous review round).
libs/ng-advanced-table/components/feature/table-column-visibility/table-column-visibility.ts Cleanly migrated from getAllLeafColumns()/toggleVisibility() to columnVisibility() selector + setColumnVisible() command; canToggle still computed locally to handle the last-visible-column guard.
libs/ng-advanced-table/render-metrics/common/contracts.type.ts NatTableRenderMetricsController updated from patchState to typed commands; correctly kept as a smaller subset contract (no sorting/rowSelection/columnVisibility) since the filter component only needs columnFilters.
libs/ng-advanced-table/render-metrics/feature/filter/filter.ts Clears filter with undefined (was null) to match TanStack's setFilterValue contract for removal; delegates to setColumnFilter which routes through onColumnFiltersChange with firstPageUpdater for automatic page reset.
libs/ng-advanced-table/src/table/table.pagination-controller.spec.ts New spec with 13 tests covering pageCount edges, navigation guards, atomic setPageSize, and filter-driven page resets; includes important comment documenting why nextPage needs a guard but previousPage does not.
libs/ng-advanced-table/src/table/table.commands-controller.spec.ts New spec with 11 tests covering sort set/replace/remove, visibility toggle with canHide guard, selection set/clear with disabled guard, and unknown-id no-ops for all commands.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Controller["NatTableUiController (NatTable)"]
        CMD["Typed Commands\nsetGlobalFilter / setColumnFilter\nsetColumnSort / setColumnVisible\nsetRowSelected / clearRowSelection\nsetPageSize / goToPage\nnextPage / previousPage"]
        SEL["Typed Selectors (Signals)\npagination / pageCount\ncanPreviousPage / canNextPage\nglobalFilter / columnFilters\nsorting / columnVisibility / rowSelection"]
    end

    subgraph Store["NatTableState"]
        CS["controlledSlice x9\n(sorting, globalFilter, columnFilters,\ncolumnVisibility, columnOrder,\ncolumnPinning, columnSizing,\nrowSelection, pagination)"]
        US["updateState()\nresolves + commits all slices"]
    end

    subgraph Factory["table-instance.factory"]
        TI["createAngularTable\nwith onXxxChange callbacks\n-> store.updateState()"]
    end

    subgraph Services["Leaf Services"]
        IS["NatTableIntlService\n(locale + a11y text)"]
        AS["NatTableA11yService\n(ARIA ids + announcements)"]
    end

    CompControls["Companion Controls\n(NatTablePager, NatTablePageSize,\nNatTablePagination, NatTableColumnVisibility,\nNatTableSearch, NatRenderMetricsFilter)"]

    CMD -->|"delegates to table.setXxx()"| TI
    TI -->|"onChange -> updateState"| US
    US --> CS
    CS -->|"merged()"| SEL
    SEL -->|"consumed by"| CompControls
    IS -->|"localeId, resolvedAccessibilityText"| AS
    IS -->|"localeId"| Store
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph Controller["NatTableUiController (NatTable)"]
        CMD["Typed Commands\nsetGlobalFilter / setColumnFilter\nsetColumnSort / setColumnVisible\nsetRowSelected / clearRowSelection\nsetPageSize / goToPage\nnextPage / previousPage"]
        SEL["Typed Selectors (Signals)\npagination / pageCount\ncanPreviousPage / canNextPage\nglobalFilter / columnFilters\nsorting / columnVisibility / rowSelection"]
    end

    subgraph Store["NatTableState"]
        CS["controlledSlice x9\n(sorting, globalFilter, columnFilters,\ncolumnVisibility, columnOrder,\ncolumnPinning, columnSizing,\nrowSelection, pagination)"]
        US["updateState()\nresolves + commits all slices"]
    end

    subgraph Factory["table-instance.factory"]
        TI["createAngularTable\nwith onXxxChange callbacks\n-> store.updateState()"]
    end

    subgraph Services["Leaf Services"]
        IS["NatTableIntlService\n(locale + a11y text)"]
        AS["NatTableA11yService\n(ARIA ids + announcements)"]
    end

    CompControls["Companion Controls\n(NatTablePager, NatTablePageSize,\nNatTablePagination, NatTableColumnVisibility,\nNatTableSearch, NatRenderMetricsFilter)"]

    CMD -->|"delegates to table.setXxx()"| TI
    TI -->|"onChange -> updateState"| US
    US --> CS
    CS -->|"merged()"| SEL
    SEL -->|"consumed by"| CompControls
    IS -->|"localeId, resolvedAccessibilityText"| AS
    IS -->|"localeId"| Store
Loading

Reviews (4): Last reviewed commit: "refactor(docs): 🎨 improve table capabil..." | Re-trigger Greptile

…ice state

Extract the controlled-vs-internal merge rule (read: controlled ?? internal,
write: commit only when uncontrolled) into one small reusable primitive with a
two-hook read/write contract. This lets the store express the rule once per
slice instead of hand-syncing it across the merge and commit paths, without
changing behaviour. Groundwork for the RFC #226 state de-duplication.
Reduce the coupling around NatTableState without changing runtime behaviour or
the controlled/uncontrolled state feature. Four coherent changes that share the
same core files (table.ts, table.state.ts, the controller contract) and so land
together:

- De-bulk the store: collapse the 9 hand-synced controlled/internal signal
  pairs onto controlledSlice, move the TanStack table construction into a
  table-instance factory, and split intl + a11y-text derivation into a leaf
  NatTableIntlService and the existing NatTableA11yService. table.state.ts drops
  from 635 to ~542 code lines; the max-lines disable is rewritten (kept, scoped)
  to name the genuinely irreducible residual.

- Typed-command facade: replace the raw-Updater-leaking
  NatTableUiController.patchState with typed commands (setGlobalFilter,
  setColumnFilter, setPageSize, goToPage, nextPage, previousPage). Companion
  controls and the internal specs move off updater closures. BREAKING: patchState
  removed from the controller contract.

- A11y read-only: the a11y service no longer clears state.resizeCommit; that
  clear moves to resize start (resize lifecycle owns it), so a11y only reads.

- Read selectors: expose typed pagination/pageCount/canPreviousPage/
  canNextPage/globalFilter/columnFilters signals; migrate the pagination,
  page-size, pager, search and render-metrics filter controls off the raw table.
  table stays on the controller (deprecated) for the rich column-object reads
  that column-visibility and export still need.

Not done (deliberate): RFC #226 "one source of truth" — it would delete the
documented per-slice controlled mode that several demos rely on.
The consumer-owned search example (and its skill-doc/composition mirrors) now
call controller.setGlobalFilter(value) instead of hand-writing a
patchState({ globalFilter, pagination }) updater closure, matching the new
typed-command controller surface.
…yped commands

- replace `patchState(...)` with intent-named command methods
- introduce typed signal selectors for state reads
- deprecate raw `table` accessor for improved clarity

BREAKING CHANGE: consumers must migrate from `controller.patchState({...})`
to the corresponding command methods and use typed selectors for state reads.
- migrate main-added sort-pin spec off removed table.patchState -> table.setSorting
- drop now-unused PaginationState import in table-search
- regenerate docs registry from HEAD composition.md (setGlobalFilter/globalFilter)
Delegate pageCount to the state's resolvedPageCount, mark the raw
table accessor deprecated in favor of typed commands, route internal
command implementations through state.table instead of the public
alias, and guard nextPage/goToPage against out-of-range navigation.
…nSort

Point the sorting and responsive-capabilities guides (and the
generated docs registry mirror) at setColumnSort instead of raw
table access, and update the table-patterns skill reference to match.
Add setColumnSort, setColumnVisible, setRowSelected, and
clearRowSelection commands plus sorting/columnVisibility/rowSelection
selectors to NatTableUiController, migrate table-column-visibility and
header-actions off the deprecated raw table accessor, and extend the
testing contracts/data helpers to cover the new commands. table-export
keeps direct table access as the deliberate exception (export needs
the raw row/column model).
@F0rty-Tw0
F0rty-Tw0 marked this pull request as ready for review July 10, 2026 19:24
@VaggelisKa

VaggelisKa commented Jul 27, 2026 •

Copy link
Copy Markdown
Owner

i think the discussion here is the following. Do we want tanstack to be the adapter or the reducer? This architecture has it more as the reducer and central state store. What i see:

Pros

  1. Exposes a single transition function for states that need many. For example, setColumnSort and a header click produce identical states by construction. Now we can propagate both by a single method etc
  2. We get library invariants for free. toggleVisibility handling group columns, setFilterValue resolving filterFn and so on
  3. Small and clearer methods
  4. Separation of state derivation. "We own the state, tanstack derives it", and a clear split between the 2.

Cons

  1. We create our own source of truth and external API. The mental model of "If you know tanstack table you can just start using this library" gets lost
  2. Behavioural version coupling. A patch release changing multi-sort cycling could silently change what setColumnSort means.
  3. Leaving TanStack becomes impractical. Since we wire it into every write.

I'd say this PR establishes the better fit for this library (going more reducer mode). But i'd say we need to make sure that It's clearly stated.

@sp90 What is your take on this?

@VaggelisKa

Copy link
Copy Markdown
Owner

What i would improve on:

  1. The facade could be moved into domain logic. table.ts should be reserved for input/outputs and dom handlers as also stated in the agents.md
  2. We should probably read table.getState().columnVisibility inside the computed so it depends on the slice it reports on. Then generalise it: selectors take their reactivity from their state slice, never from a library accessor's memo. Because claude caught a nasty trap that we might land on in the future.
    -> "Bug — columnVisibility() derives from getAllLeafColumns(), which table-core memoizes on column identity and order, not visibility. Hiding a column doesn't invalidate it. It appears to work only because normalizeColumnOrder allocates a fresh array on every state change, incidentally invalidating the memo."
  3. Finish the controller.table deprecation. It can't complete today because NatTableExportContext hands out the raw table by contract, figure out the best way forward
  4. How and when should we handle the two way state? the controlled ?? passed. The RFC mentioned one of the wins would be the removal of it right?

@sp90

sp90 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

I love the fact that we kind of centralize state big win, but im also kind of thoughtful of throwing the tanstack familarity out the window as you also point to @VaggelisKa. I dont paticually think its a need but it is a tradeoff we need to agree we're happy with

This branch was successfully deployed

1 active deployment
Preview — a42fda12 Deployed Jul 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor (RFC): re-architect NatTable state to one source of truth, a typed-command facade, and one-way flow

3 participants