@softwarity/store
v1.19.2
Published
Angular store module by softwarity
Maintainers
Readme
@softwarity/store
@softwarity/store lets you persist UI preferences (visible columns, sort order, page size, filters, sidebar state…) directly in the browser — no server-side persistence needed. No API endpoints, no database tables, no extra round-trips: everything stays client-side, where UI preferences belong.
A single decorator or function call is all it takes. Annotate a property — or wrap an object with localStored() / sessionStored() — and every mutation (including nested properties and array methods) is automatically persisted to browser storage and restored on reload. No manual getItem / setItem, no boilerplate serialization logic — @softwarity/store handles it all for you.
Features
- Decorator API —
@LocalStored/@SessionStoredwith deep mutation tracking and$prop()reactive signals - Stored API —
localStored()/sessionStored()— same deep tracking and$prop()signals, without annotations - Deep mutation tracking — Native
Proxy-based: nested properties, array methods, direct index assignment (arr[0] = 'x'), and new properties all trigger saves automatically - Versioning — Increment the version to discard stale data when the structure or defaults change
- User-scoped storage — Prefix storage keys with a user ID
- Cross-tab sync —
onStorageChange()for localStorage changes from other tabs - SSR compatible —
FakeStoragefallback whenwindowis undefined - ~~Zoneless~~ — No dependency on zone.js
Installation
npm install @softwarity/storeSetup
Standalone (recommended)
import { provideStore } from '@softwarity/store';
export const appConfig = {
providers: [provideStore()]
};Module-based (legacy)
import { StoreModule } from '@softwarity/store';
@NgModule({ imports: [StoreModule] })
export class AppModule {}Decorator API
Annotate class fields for automatic persistence. Nested property changes, array methods, and direct index assignments trigger saves automatically.
import { LocalStored, SessionStored } from '@softwarity/store';
@Component({ ... })
export class MyComponent {
// localStorage — version 1
// Increment to discard stale data when the structure or defaults change.
@LocalStored(1)
tableConfig = {
columns: ['name', 'age'],
sort: { active: 'name', direction: 'asc' },
pageSize: 25
};
// sessionStorage (no version needed, data is short-lived)
@SessionStored()
filters = { search: '', category: 'all' };
// Custom storage key (instead of auto-generated ClassName.property)
@LocalStored(1, 'shared-config')
sharedConfig = { theme: 'dark' };
// Note: decorators auto-generate the key as ClassName.property.
// Use the optional storageKey parameter to override it.
}Stored API — localStored / sessionStored
Same deep mutation tracking as decorators, but without annotations. Returns a StoredSignal<T> with plain property access and $prop() reactive signals. Requires Angular injection context.
import { localStored, sessionStored } from '@softwarity/store';
@Component({ ... })
export class MyComponent {
// localStorage + deep tracking + versioning
// storageKey is required: functions don't have access to ClassName.property,
// unlike decorators which auto-generate it.
config = localStored(
{ columns: ['name', 'age'], sort: { active: 'name', direction: 'asc' } },
{ storageKey: 'table-config', version: 1 }
);
// sessionStorage + deep tracking
wizard = sessionStored(
{ step: 1, draft: '' },
{ storageKey: 'wizard-state' }
);
}Deep mutation tracking
All stored objects are wrapped with native Proxy. All mutations trigger automatic saves:
// All of these trigger a save:
this.config.sort.direction = 'desc'; // nested property
this.config.columns.push('email'); // array push
this.config.columns.splice(0, 1); // array splice
this.config.columns.reverse(); // array reverse
this.config.columns.sort(); // array sort
this.config.columns[0] = 'email'; // direct index assignment
this.config.sort.newProp = 'value'; // new property on object
// This does NOT trigger a save:
this.config = { ... }; // root reassignment (use property setters)$prop() reactive signals
Both APIs expose $-prefixed signals at every level of depth, ideal for template bindings:
<!-- Top-level signals -->
<mat-header-row *matHeaderRowDef="config.$columns()"></mat-header-row>
<mat-paginator
[pageSize]="config.$pageSize()"
[pageIndex]="config.$pageIndex()"
(page)="onPage($event)">
</mat-paginator>
<!-- Nested object signals -->
<mat-table matSort
[matSortActive]="config.sort.$column()"
[matSortDirection]="config.sort.$direction()"
(matSortChange)="onSort($event)">
</mat-table>Nested signals are available at any depth (config.a.b.$c()). Each signal returns the plain value of the property (via toPlain), and updates reactively whenever the property or any of its descendants is mutated.
Version management
Increment the version whenever the object structure or the default values change. Old stored data is discarded and replaced with the new defaults.
// Version 1: initial structure
@LocalStored(1)
config = { columns: ['name', 'age'], sort: 'asc' };
// Version 2: added filter — stale browser data is discarded
@LocalStored(2)
config = { columns: ['name', 'age'], sort: 'asc', filter: null };
// Version 3: default columns changed — users get the new defaults
@LocalStored(3)
config = { columns: ['name', 'age', 'email'], sort: 'asc', filter: null };Only @LocalStored and localStored() support versioning. @SessionStored and sessionStored() do not need it (data is short-lived).
User-scoped storage
Browser storage (localStorage / sessionStorage) is shared across all users on the same browser profile. On shared workstations or kiosk machines, multiple people may log in to the same app one after another. Without scoping, user A's persisted preferences (columns, filters, sort order…) would leak to user B.
By providing a userId signal, every storage key is automatically prefixed with the current user's identifier (userId_storageKey). When the user changes (login / logout / switch), the library reloads each stored object from the new user's namespace — so preferences follow the user, not the machine.
Standalone
import { provideStore } from '@softwarity/store';
import { inject } from '@angular/core';
export const appConfig = {
providers: [
provideStore({ userId: () => inject(AuthService).userId })
]
};Module-based (legacy)
import { StoreModule, USER_ID } from '@softwarity/store';
import { inject } from '@angular/core';
@NgModule({
imports: [StoreModule],
providers: [
{ provide: USER_ID, useFactory: () => inject(AuthService).userId }
]
})
export class AppModule {}Storage key format
| API | Without userId | With userId |
|---|---|---|
| Decorators (auto) | ClassName.property | userId_ClassName.property |
| Decorators (custom storageKey) | storageKey | userId_storageKey |
| localStored / sessionStored | storageKey | userId_storageKey |
Cross-tab sync
Listen for localStorage changes from other browser tabs:
import { onStorageChange } from '@softwarity/store';
@Component({ ... })
export class MyComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
onStorageChange<{ theme: string }>('app-config', newValue => {
console.log('Changed in another tab:', newValue);
}, this.destroyRef); // auto-cleanup on destroy
}
}Note: Only works with localStorage (
StorageEventspec limitation).
Utilities
import { clearLocalStorage, clearSessionStorage } from '@softwarity/store';
// Clear all
clearLocalStorage();
// Clear only keys with a specific prefix (e.g. user-scoped data)
clearLocalStorage('alice_');
clearSessionStorage('alice_');SSR support
The library provides a FakeStorage class used automatically when window is undefined (SSR context). You can also import it directly:
import { FakeStorage } from '@softwarity/store';License
MIT
