├── src
├── assets
│ └── .gitkeep
├── app
│ ├── app.component.scss
│ ├── app.routes.ts
│ ├── app.config.ts
│ ├── app.component.html
│ └── app.component.ts
├── favicon.ico
├── styles.scss
├── main.ts
└── index.html
├── .npmrc
├── projects
└── ngrx-signals-storage
│ ├── src
│ ├── public-api.ts
│ └── lib
│ │ ├── config.ts
│ │ ├── with-storage.ts
│ │ └── with-storage.spec.ts
│ ├── ng-package.json
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.spec.json
│ ├── tsconfig.lib.json
│ └── package.json
├── .editorconfig
├── renovate.json
├── tsconfig.app.json
├── tsconfig.spec.json
├── .gitignore
├── package.json
├── LICENSE
├── tsconfig.json
├── .github
└── workflows
│ └── workflow.yml
├── angular.json
└── README.md
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larscom/ngrx-signals-storage/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
2 | save-exact=true
3 | package-lock=true
4 | legacy-peer-deps=true
5 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | export const routes: Routes = [];
4 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/src/public-api.ts:
--------------------------------------------------------------------------------
1 | export { type Config } from './lib/config'
2 | export { withStorage } from './lib/with-storage'
3 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/ngrx-signals-storage",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | }
7 | }
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig } from '@angular/core'
2 | import { provideRouter } from '@angular/router'
3 | import { routes } from './app.routes'
4 |
5 | export const appConfig: ApplicationConfig = {
6 | providers: [provideRouter(routes)]
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
Counter
2 | {{ store.count() }}
3 |
4 | Date
5 | {{ store.date() }}
6 |
7 | Test
8 | {{ store.test() }}
9 |
10 | Unique numbers
11 | {{ uniqueNumbers }}
12 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { appConfig } from './app/app.config';
3 | import { AppComponent } from './app/app.component';
4 |
5 | bootstrapApplication(AppComponent, appConfig)
6 | .catch((err) => console.error(err));
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended",
5 | ":disableDependencyDashboard"
6 | ],
7 | "packageRules": [
8 | {
9 | "description": "Automerge non-major updates",
10 | "matchUpdateTypes": ["minor", "patch"],
11 | "automerge": true
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | @larscom/ngrx-signals-storage
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.lib.json",
5 | "compilerOptions": {
6 | "declarationMap": false
7 | },
8 | "angularCompilerOptions": {
9 | "compilationMode": "partial"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/app",
7 | "types": []
8 | },
9 | "include": [
10 | "src/**/*.ts"
11 | ],
12 | "exclude": [
13 | "src/**/*.spec.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/spec",
7 | "types": [
8 | "vitest/globals"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.d.ts",
13 | "src/**/*.spec.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "../../tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "../../out-tsc/spec",
7 | "types": [
8 | "vitest/globals"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.d.ts",
13 | "src/**/*.spec.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "../../tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "../../out-tsc/lib",
7 | "declaration": true,
8 | "declarationMap": true,
9 | "inlineSources": true,
10 | "types": []
11 | },
12 | "include": [
13 | "src/**/*.ts"
14 | ],
15 | "exclude": [
16 | "**/*.spec.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngrx-signals-storage",
3 | "version": "4.0.2",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "test": "ng test ngrx-signals-storage",
8 | "test:watch": "ng test ngrx-signals-storage --watch",
9 | "build": "ng build ngrx-signals-storage"
10 | },
11 | "private": true,
12 | "dependencies": {
13 | "@angular/common": "^21.0.0",
14 | "@angular/compiler": "^21.0.0",
15 | "@angular/core": "^21.0.0",
16 | "@angular/forms": "^21.0.0",
17 | "@angular/platform-browser": "^21.0.0",
18 | "@angular/router": "^21.0.0",
19 | "@ngrx/signals": "21.0.0",
20 | "rxjs": "~7.8.0",
21 | "tslib": "^2.3.0"
22 | },
23 | "devDependencies": {
24 | "@angular/build": "^21.0.0",
25 | "@angular/cli": "^21.0.0",
26 | "@angular/compiler-cli": "^21.0.0",
27 | "jsdom": "^27.2.0",
28 | "ng-packagr": "^21.0.0",
29 | "typescript": "~5.9.2",
30 | "vitest": "^4.0.8"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@larscom/ngrx-signals-storage",
3 | "version": "0.0.0",
4 | "description": "Save signal state (@ngrx/signals) to localstorage/sessionstorage and restore the state on page load (with SSR support).",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/larscom/ngrx-signals-storage.git"
8 | },
9 | "publishConfig": {
10 | "access": "public"
11 | },
12 | "keywords": [
13 | "angular",
14 | "ngrx",
15 | "redux",
16 | "store",
17 | "state",
18 | "signals",
19 | "storage",
20 | "localstorage",
21 | "sessionstorage",
22 | "reactive"
23 | ],
24 | "author": "Lars Kniep",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/larscom/ngrx-signals-storage/issues"
28 | },
29 | "homepage": "https://github.com/larscom/ngrx-signals-storage#readme",
30 | "peerDependencies": {
31 | "@angular/core": ">=17.0.0",
32 | "@ngrx/signals": ">=18.0.0"
33 | },
34 | "dependencies": {
35 | "tslib": "^2.3.0"
36 | },
37 | "sideEffects": false
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Lars Kniep
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | /**
3 | * These keys will not get saved to storage
4 | */
5 | excludeKeys: Array
6 |
7 | /**
8 | * Serializer for the state, by default it uses `JSON.stringify()`
9 | * @param state the last state known before it gets saved to storage
10 | */
11 | serialize: (state: T) => string
12 |
13 | /**
14 | * Deserializer for the state, by default it uses `JSON.parse()`
15 | * @param state the last state known from the storage location
16 | */
17 | deserialize: (state: string) => T
18 |
19 | /**
20 | * Save to storage will only occur when this function returns true
21 | * @param state the last state known before it gets saved to storage
22 | */
23 | saveIf: (state: T) => boolean
24 |
25 | /**
26 | * Function that gets executed on a storage error (get/set)
27 | * @param error the error that occurred
28 | */
29 | error: (error: any) => void
30 | }
31 |
32 | export const defaultConfig: Config = {
33 | excludeKeys: [],
34 |
35 | serialize: (state: any) => JSON.stringify(state),
36 |
37 | deserialize: (state: string) => JSON.parse(state),
38 |
39 | saveIf: (state: any) => true,
40 |
41 | error: (error: any) => console.error(error)
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "strict": true,
6 | "noImplicitOverride": true,
7 | "noPropertyAccessFromIndexSignature": true,
8 | "noImplicitReturns": true,
9 | "paths": {
10 | "@larscom/ngrx-signals-storage": [
11 | "./projects/ngrx-signals-storage/src/public-api"
12 | ]
13 | },
14 | "noFallthroughCasesInSwitch": true,
15 | "skipLibCheck": true,
16 | "isolatedModules": true,
17 | "experimentalDecorators": true,
18 | "importHelpers": true,
19 | "target": "ES2022",
20 | "module": "preserve"
21 | },
22 | "angularCompilerOptions": {
23 | "enableI18nLegacyMessageIdFormat": false,
24 | "strictInjectionParameters": true,
25 | "strictInputAccessModifiers": true,
26 | "strictTemplates": true,
27 | "disableTypeScriptVersionCheck": true
28 | },
29 | "files": [],
30 | "references": [
31 | {
32 | "path": "./tsconfig.app.json"
33 | },
34 | {
35 | "path": "./tsconfig.spec.json"
36 | },
37 | {
38 | "path": "./projects/ngrx-signals-storage/tsconfig.lib.json"
39 | },
40 | {
41 | "path": "./projects/ngrx-signals-storage/tsconfig.spec.json"
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Component, inject } from '@angular/core'
3 | import { withStorage } from '@larscom/ngrx-signals-storage'
4 | import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'
5 |
6 | export const CounterStore = signalStore(
7 | withState({
8 | count: 100,
9 | test: 5,
10 | date: new Date(),
11 | unique: new Set([1, 1, 3, 3])
12 | }),
13 | withStorage('state', () => sessionStorage, {
14 | excludeKeys: ['test'],
15 | serialize: (state) => JSON.stringify({ ...state, unique: Array.from(state.unique) }),
16 | deserialize: (stateString) => {
17 | const state = JSON.parse(stateString)
18 | return {
19 | ...state,
20 | unique: new Set(state.unique)
21 | }
22 | }
23 | }),
24 | withMethods(({ count, ...store }) => ({
25 | setDate(date: Date) {
26 | patchState(store, { date })
27 | },
28 | increment(by: number) {
29 | patchState(store, { count: count() + by })
30 | }
31 | }))
32 | )
33 |
34 | @Component({
35 | selector: 'app-root',
36 | standalone: true,
37 | imports: [],
38 | providers: [CounterStore],
39 | templateUrl: './app.component.html',
40 | styleUrl: './app.component.scss'
41 | })
42 | export class AppComponent {
43 | store = inject(CounterStore)
44 |
45 | constructor() {
46 | setTimeout(() => this.store.increment(100), 3000)
47 | }
48 |
49 | get uniqueNumbers() {
50 | return Array.from(this.store.unique())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.github/workflows/workflow.yml:
--------------------------------------------------------------------------------
1 | name: workflow
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*.*.*'
7 | branches:
8 | - '**'
9 | merge_group:
10 | pull_request:
11 | branches:
12 | - main
13 |
14 | env:
15 | CI: true
16 |
17 | jobs:
18 | test:
19 | runs-on: ubuntu-latest
20 | env:
21 | TZ: Europe/Amsterdam
22 | steps:
23 | - uses: actions/checkout@v6
24 | - uses: actions/setup-node@v6
25 | with:
26 | node-version: 24
27 | registry-url: https://registry.npmjs.org/
28 | - run: |
29 | npm ci --ignore-scripts --legacy-peer-deps
30 | npm run build
31 | npm run test
32 |
33 | publish:
34 | if: startsWith(github.ref, 'refs/tags/')
35 | needs: [test]
36 | runs-on: ubuntu-latest
37 | env:
38 | TZ: Europe/Amsterdam
39 | steps:
40 | - uses: actions/checkout@v6
41 | - uses: actions/setup-node@v6
42 | with:
43 | node-version: 24
44 | registry-url: https://registry.npmjs.org/
45 | env:
46 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
47 | - run: |
48 | npm ci --ignore-scripts --legacy-peer-deps
49 | npm run build
50 | - run: |
51 | cp README.md dist/ngrx-signals-storage
52 | cd dist/ngrx-signals-storage
53 |
54 | version=${{ github.ref_name }}
55 | sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$version\"/" ./package.json
56 |
57 | npm publish
58 |
--------------------------------------------------------------------------------
/projects/ngrx-signals-storage/src/lib/with-storage.ts:
--------------------------------------------------------------------------------
1 | import { isPlatformServer } from '@angular/common'
2 | import { effect, inject, PLATFORM_ID } from '@angular/core'
3 | import {
4 | EmptyFeatureResult,
5 | getState,
6 | patchState,
7 | SignalStoreFeature,
8 | signalStoreFeature,
9 | SignalStoreFeatureResult,
10 | withHooks
11 | } from '@ngrx/signals'
12 | import { Config, defaultConfig } from './config'
13 |
14 | /**
15 | * The `withStorage` function that lets you save the state to localstorage/sessionstorage
16 | * and rehydrate the state upon page load.
17 | *
18 | * @param key the key under which the state should be saved into `Storage`
19 | * @param storage function that returns an implementation of the `Storage` interface, like: `sessionStorage` or `localStorage`
20 | *
21 | * @example
22 | * export const CounterStore = signalStore(
23 | * withState({
24 | * count: 0
25 | * }),
26 | * withStorage('myKey', () => sessionStorage)
27 | * )
28 | */
29 | export function withStorage(
30 | key: string,
31 | storage: () => Storage,
32 | config?: Partial>
33 | ): SignalStoreFeature {
34 | return signalStoreFeature(
35 | withHooks({
36 | onInit(store, platformId = inject(PLATFORM_ID)) {
37 | if (isPlatformServer(platformId)) {
38 | return
39 | }
40 |
41 | const cfg = { ...defaultConfig, ...config }
42 | const item = getFromStorage(key, storage(), cfg)
43 | const stateFromStorage: T['state'] | null = item ? cfg.deserialize(item) : null
44 |
45 | if (stateFromStorage != null) {
46 | const stateSignalKeys = Object.keys(store)
47 | const state = stateSignalKeys.reduce((state, key) => {
48 | const value = stateFromStorage[key as keyof T['state']]
49 | return value !== undefined
50 | ? {
51 | ...state,
52 | [key]: value
53 | }
54 | : state
55 | }, getState(store))
56 |
57 | patchState(store, state)
58 | }
59 |
60 | effect(() => {
61 | const state = structuredClone