feat(components): add a service for inspecting and managing saved state (#DS-3197) - #1996
Draft
lskramarov wants to merge 3 commits into
Draft
feat(components): add a service for inspecting and managing saved state (#DS-3197)#1996lskramarov wants to merge 3 commits into
lskramarov wants to merge 3 commits into
Conversation
…istence (#DS-3197) Accordion state was persisted in a map keyed by item id but restored by matching item value, so the map key was never read back. Item ids come from a global counter, so a value that got a new id on a later render gained a second entry: closing a section updated only the current one, and the stale entry reopened it on the next load. The blob also grew without bound, `single` mode picked its winner by key order and never self-corrected, and a saved-but-fully-collapsed state notified nothing at all. The persisted state is now the accordion's value — the list of expanded item values — written as a whole snapshot, which removes those failure modes structurally. A payload is normalized before use, since web storage is origin-wide and user-writable, and state persisted in the previous format is migrated while reading. The storage mechanism moved into core so other components can persist state through it: `KbqStateStore` moves opaque JSON payloads, and `kbqStateSaving()` owns the key resolution, the dev-mode warning, reading with normalization, writing, and the suppression that keeps restoring from writing the state straight back. The docs examples no longer enable state saving: they share one origin on the docs site, so their auto-generated keys collided across pages. A dedicated example covers the feature instead, and the Core page documents how to adopt it in another component. BREAKING CHANGE: the accordion-specific store API is replaced by the core one — `KBQ_ACCORDION_STATE_STORE` by `KBQ_STATE_STORE`, `KbqAccordionStateStore` by `KbqStateStore`, `KbqAccordionLocalStorageStateStore` by `KbqLocalStorageStateStore`, all exported from `@koobiq/components/core`. A store now implements `getState(key): unknown`, `setState(key, unknown)` and the new `removeState(key)`. `KbqAccordion.saveItemState()` and `KbqAccordionItem.getState()` are replaced by `saveState()` and `clearSavedState()`, and `KbqAccordionState` is now `string[]`.
`KbqAccordion.useStateSaving` now defaults to `true`: an accordion nobody
configured remembers which sections the user left open and restores them on
the next render. `[useStateSaving]="false"` opts out.
The default is only defensible because the key no longer depends on
instantiation order. `kbqStructuralStateSavingKey` derives it from where the
host sits in the document — the chain of tag names up to `<body>`, cut short
by the first author-written `id`, which becomes the anchor — so markup above
that `id` can be restructured without moving the key, and an `id` pins it as
well as `stateSavingKey` does. A host outside the document resolves to no key
at all: nothing is persisted, and dev mode says so. The strategy is
replaceable through `KBQ_STATE_SAVING_KEY_RESOLVER`.
The payload was as unstable as the key: an item with no `value` persisted
`kbq-accordion-item-${counter}`. It now persists its position inside its own
accordion. `aria-controls` and the trigger/content ids are built on `item.id`,
so they are unaffected.
Web-storage entries move under a `kbq.state.` prefix, so one cannot collide
with a key the application owns, and carry the time they were written. An
entry that goes `KBQ_STATE_SAVING_TTL` (90 days by default) without being
written or read is collected the next time a store is constructed, which is
what keeps keys stranded by a restructuring from accumulating; reading
refreshes an entry, so state that is visited but never changed does not expire
under an active user. An entry written by 20.2.0 under the bare key is still
read, but never rewritten or removed — an unprefixed key is not necessarily
ours, and an application storing its own `settings` must not lose it to a
component keyed `stateSavingKey="settings"`.
The dev-mode warning about a missing `stateSavingKey` is gone: an accordion
nobody configured is the ordinary case now. A warning is still logged when no
key can be derived at all.
The accordion e2e host opts out explicitly — four sibling accordions sharing
state across Playwright runs would make the screenshots flaky.
BREAKING CHANGE: `KbqAccordion.useStateSaving` defaults to `true`, so
`defaultValue` applies to the first visit only and an accordion persists
unless it passes `[useStateSaving]="false"`. `KbqAccordionItem.value` falls
back to the item's position inside its accordion instead of its id.
`KbqStateSavingConfig.fallbackKey` is removed; `KBQ_STATE_SAVING_KEY_RESOLVER`
replaces it. Web-storage entries are written under a `kbq.state.` prefix and
expire after `KBQ_STATE_SAVING_TTL`. Reported by the warn-only
`accordion-state-saving-default` migration schematic.
…te (#DS-3197) `KbqStateSavingService` is the application-wide view of what components have persisted. It is provided in the root injector, and reports snapshots: `components()` for who persists and what they hold, `keys()` for what the store holds, `orphans()` for the entries no live component claims. `remove()`, `clearOrphans()` and `clear()` manage them, `write()` imports a payload, and `setEnabled(false)` takes persistence away from every component at once — the switch behind a "do not remember my interface" setting. Two sources, because neither covers the other. `KBQ_STATE_STORE` can be replaced in a component's own `providers`, and the root store knows nothing about that one, so every `kbqStateSaving()` controller registers itself and arrives carrying its own store. The store in turn knows about entries no component claims: a component whose surrounding markup changed writes under a new key and strands the old entry. `KbqStateStore` gains two optional members, following `KbqThemeStore.changes`: `keys()`, which a store that cannot enumerate — a backend, most commonly — leaves out, and `changes`, which reports a write from another tab. The web-storage stores implement both. Snapshots rather than signals: components register while their host is being created, which happens during change detection, and a signal written then is read back by a view that has already been checked (NG0100). `changes` says that something changed so a consumer can take a fresh snapshot, and a component persisting its own state reports through it too — that write does not pass through the service and would otherwise be the one change a panel never hears about. A compound removal reports once rather than once per component it reaches. The application-wide switch gates reading and writing but deliberately not a component clearing its own state: it means "stop remembering", and removing what was already remembered carries that out rather than working against it. A component's own `useStateSaving` still governs what it reports and can clear. Also fixes the resolved key being memoized with `??=`, which left a host asked before it was in the document stuck with the empty key it resolved to then. Exposing `KbqStateSaving.key` made that reachable.
|
Visit the preview URL for this PR (updated for commit 6509185): https://koobiq-next--prs-1996-puv6dx6g.web.app (expires Mon, 07 Sep 2026 11:23:37 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c9e37e518febda70d0317d07e8ceb35ac43c534c |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.