├── .editorconfig ├── .env ├── .github └── workflows │ ├── build.yml │ └── deploy-docs.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── DEVELOPMENT.md ├── LICENSE ├── Logo Social Image.ai ├── README.md ├── apps └── demo │ ├── e2e │ ├── conditional.spec.ts │ ├── devtools.spec.ts │ ├── feature-factory.spec.ts │ ├── immutable-state.spec.ts │ └── reset.spec.ts │ ├── eslint.config.cjs │ ├── jest.config.ts │ ├── playwright.config.ts │ ├── project.json │ ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── category.store.ts │ │ ├── core │ │ │ └── sidebar │ │ │ │ ├── sidebar.component.css │ │ │ │ ├── sidebar.component.html │ │ │ │ └── sidebar.component.ts │ │ ├── devtools │ │ │ ├── todo-detail.component.ts │ │ │ ├── todo-store.ts │ │ │ └── todo.component.ts │ │ ├── feature-factory │ │ │ └── feature-factory.component.ts │ │ ├── flight-search-data-service-dynamic │ │ │ ├── flight-booking.store.ts │ │ │ ├── flight-edit.component.html │ │ │ ├── flight-edit.component.ts │ │ │ ├── flight-search.component.html │ │ │ └── flight-search.component.ts │ │ ├── flight-search-data-service-simple │ │ │ ├── flight-booking-simple.store.ts │ │ │ ├── flight-edit-simple.component.html │ │ │ ├── flight-edit-simple.component.ts │ │ │ ├── flight-search-simple.component.html │ │ │ └── flight-search-simple.component.ts │ │ ├── flight-search-redux-connector │ │ │ ├── +state │ │ │ │ ├── actions.ts │ │ │ │ ├── model.ts │ │ │ │ ├── redux.ts │ │ │ │ └── store.ts │ │ │ ├── flight-search.component.html │ │ │ └── flight-search.component.ts │ │ ├── flight-search-with-pagination │ │ │ ├── flight-search-with-pagination.component.html │ │ │ ├── flight-search-with-pagination.component.ts │ │ │ └── flight-store.ts │ │ ├── flight-search │ │ │ ├── flight-search.component.html │ │ │ ├── flight-search.component.ts │ │ │ ├── flight-store.ts │ │ │ └── flight.ts │ │ ├── immutable-state │ │ │ └── immutable-state.component.ts │ │ ├── lazy-routes.ts │ │ ├── reset │ │ │ ├── todo-store.ts │ │ │ └── todo.component.ts │ │ ├── shared │ │ │ ├── flight-card.component.html │ │ │ ├── flight-card.component.ts │ │ │ ├── flight.service.ts │ │ │ ├── flight.ts │ │ │ └── todo.service.ts │ │ ├── todo-indexeddb-sync │ │ │ ├── synced-todo-store.ts │ │ │ ├── todo-indexeddb-sync.component.html │ │ │ ├── todo-indexeddb-sync.component.scss │ │ │ └── todo-indexeddb-sync.component.ts │ │ ├── todo-storage-sync │ │ │ ├── synced-todo-store.ts │ │ │ ├── todo-storage-sync.component.html │ │ │ ├── todo-storage-sync.component.scss │ │ │ └── todo-storage-sync.component.ts │ │ └── with-conditional │ │ │ └── conditional.component.ts │ ├── assets │ │ └── .gitkeep │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── styles.css │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── commitlint.config.js ├── devtools.png ├── docs ├── .gitignore ├── README.md ├── docs │ ├── create-redux-state.md │ ├── extensions.md │ ├── with-call-state.md │ ├── with-conditional.md │ ├── with-data-service.md │ ├── with-devtools.md │ ├── with-feature-factory.md │ ├── with-immutable-state.md │ ├── with-redux.md │ ├── with-reset.md │ ├── with-storage-sync.md │ └── with-undo-redo.md ├── docusaurus.config.ts ├── package.json ├── pnpm-lock.yaml ├── sidebars.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── devtools.png │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── favicon_io.zip │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── site.webmanifest │ │ └── social.png ├── tsconfig.json └── typedoc.json ├── eslint.config.cjs ├── integration-tests.sh ├── jest.config.ts ├── jest.preset.js ├── libs └── ngrx-toolkit │ ├── README.md │ ├── eslint.config.cjs │ ├── jest.config.ts │ ├── ng-package.json │ ├── package.json │ ├── project.json │ ├── redux-connector │ ├── docs │ │ └── README.md │ ├── index.ts │ ├── ng-package.json │ └── src │ │ └── lib │ │ ├── create-redux.ts │ │ ├── model.ts │ │ ├── rxjs-interop │ │ └── redux-method.ts │ │ ├── signal-redux-store.ts │ │ └── util.ts │ ├── src │ ├── index.ts │ ├── lib │ │ ├── assertions │ │ │ └── assertions.ts │ │ ├── devtools │ │ │ ├── features │ │ │ │ ├── with-disabled-name-indicies.ts │ │ │ │ ├── with-glitch-tracking.ts │ │ │ │ └── with-mapper.ts │ │ │ ├── internal │ │ │ │ ├── current-action-names.ts │ │ │ │ ├── default-tracker.ts │ │ │ │ ├── devtools-feature.ts │ │ │ │ ├── devtools-syncer.service.ts │ │ │ │ ├── glitch-tracker.service.ts │ │ │ │ └── models.ts │ │ │ ├── provide-devtools-config.ts │ │ │ ├── rename-devtools-name.ts │ │ │ ├── tests │ │ │ │ ├── action-name.spec.ts │ │ │ │ ├── basic.spec.ts │ │ │ │ ├── connecting.spec.ts │ │ │ │ ├── helpers.spec.ts │ │ │ │ ├── naming.spec.ts │ │ │ │ ├── provide-devtools-config.spec.ts │ │ │ │ ├── types.spec.ts │ │ │ │ ├── with-devtools.spec.ts │ │ │ │ ├── with-glitch-tracking.spec.ts │ │ │ │ └── with-mapper.spec.ts │ │ │ ├── update-state.ts │ │ │ ├── with-dev-tools-stub.ts │ │ │ └── with-devtools.ts │ │ ├── immutable-state │ │ │ ├── deep-freeze.ts │ │ │ ├── is-dev-mode.ts │ │ │ ├── tests │ │ │ │ └── with-immutable-state.spec.ts │ │ │ └── with-immutable-state.ts │ │ ├── shared │ │ │ ├── prettify.ts │ │ │ ├── signal-store-models.ts │ │ │ └── throw-if-null.ts │ │ ├── storage-sync │ │ │ ├── features │ │ │ │ ├── with-indexeddb.ts │ │ │ │ ├── with-local-storage.ts │ │ │ │ └── with-session-storage.ts │ │ │ ├── internal │ │ │ │ ├── indexeddb.service.ts │ │ │ │ ├── local-storage.service.ts │ │ │ │ ├── models.ts │ │ │ │ └── session-storage.service.ts │ │ │ ├── tests │ │ │ │ ├── indexeddb.service.spec.ts │ │ │ │ └── with-storage-sync.spec.ts │ │ │ └── with-storage-sync.ts │ │ ├── with-call-state.spec.ts │ │ ├── with-call-state.ts │ │ ├── with-conditional.spec.ts │ │ ├── with-conditional.ts │ │ ├── with-data-service.spec.ts │ │ ├── with-data-service.ts │ │ ├── with-feature-factory.spec.ts │ │ ├── with-feature-factory.ts │ │ ├── with-pagination.spec.ts │ │ ├── with-pagination.ts │ │ ├── with-redux.spec.ts │ │ ├── with-redux.ts │ │ ├── with-reset.spec.ts │ │ ├── with-reset.ts │ │ ├── with-undo-redo.spec.ts │ │ └── with-undo-redo.ts │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── logo.ai ├── logo.png ├── migrations.json ├── nx.json ├── package.json ├── pnpm-lock.yaml ├── read-supported-versions.js └── tsconfig.base.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Nx 18 enables using plugins to infer targets by default 2 | # This is disabled for existing workspaces to maintain compatibility 3 | # For more info, see: https://nx.dev/concepts/inferred-tasks 4 | NX_ADD_PLUGINS=false -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | with: 16 | version: 10 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '18' 20 | cache: "pnpm" 21 | - run: pnpm install --frozen-lockfile 22 | - run: pnpm run lint:all 23 | - run: pnpm run test:all 24 | - run: pnpm run test:e2e 25 | - run: pnpm run build:all 26 | - run: ./integration-tests.sh 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | defaults: 4 | run: 5 | working-directory: ./docs 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | # Review gh actions docs if you want to further define triggers, paths, etc 12 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 13 | 14 | jobs: 15 | build: 16 | name: Build Docusaurus 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: pnpm/action-setup@v4 23 | with: 24 | version: 10 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 18 28 | cache: pnpm 29 | 30 | - name: Install dependencies for toolkit 31 | run: cd .. && pnpm install --frozen-lockfile 32 | 33 | - name: Install dependencies 34 | run: pnpm install --frozen-lockfile 35 | - name: Build website 36 | run: pnpm build 37 | 38 | - name: Upload Build Artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: docs/build 42 | 43 | deploy: 44 | name: Deploy to GitHub Pages 45 | needs: build 46 | 47 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 48 | permissions: 49 | pages: write # to deploy to Pages 50 | id-token: write # to verify the deployment originates from an appropriate source 51 | 52 | # Deploy to the github-pages environment 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v4 62 | -------------------------------------------------------------------------------- /.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 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .nx 42 | .angular 43 | /versions.txt 44 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | 2 | pnpm --no -- commitlint --edit ${1} 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | pnpm verify:all 3 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,json,html,scss,md}": ["prettier --write"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/.npmrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | .angular 6 | pnpm-lock.yaml 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "ms-playwright.playwright", 7 | "dbaeumer.vscode-eslint" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true 9 | }, 10 | "hide-files.files": [] 11 | } -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | Every new feature needs to come with following things: 2 | 3 | An RFC on GitHub where it is decided if the feature is going to be implemented or not, including some basic implementation and design details. 4 | 5 | SignalStore features start with an `with`, followed by the feature name. For example, `with-encryption`. You create it the source file in `libs/ngrx-toolkit/src/lib`. 6 | 7 | If the feature does not fit into one file, divide it up into multiple files and put them into a folder with the same name as the feature. For example, as it is done with `withDevtools()`. 8 | 9 | In case the feature uses third-party libraries, we need to provide a secondary entry point. An existing example is the `redux-connector` in `libs/ngrx-toolkit/redux-connector`. 10 | 11 | Further necessary things for a new feature: 12 | 13 | - Test 14 | - Unit Tests 15 | - E2E Tests 16 | - Documentation in 17 | - `/docs` 18 | - as well at the function itself via JSDoc 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Softarc Consulting GmbH 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 | -------------------------------------------------------------------------------- /Logo Social Image.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/Logo Social Image.ai -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgRx Toolkit 2 | 3 | [![npm](https://img.shields.io/npm/v/%40angular-architects%2Fngrx-toolkit.svg)](https://www.npmjs.com/package/%40angular-architects%2Fngrx-toolkit) 4 | 5 | 6 | 7 | NgRx Toolkit is a set of extensions to the NgRx Signals Store, like 8 | 9 | - Devtools: Integration into Redux Devtools 10 | - Redux: Possibility to use the Redux Pattern (Reducer, Actions, Effects) 11 | - Storage Sync: Synchronize the Store with Web Storage 12 | - [Redux Connector: Map NgRx Store Actions to a present Signal Store](libs/ngrx-toolkit/redux-connector/docs/README.md) 13 | 14 | To install it, run 15 | 16 | ```shell 17 | npm i @angular-architects/ngrx-toolkit 18 | ``` 19 | 20 | For a more detailed guide on installation, setup, and usage, head to the [**Documentation**](https://ngrx-toolkit.angulararchitects.io/). 21 | 22 | ## https://ngrx-toolkit.angulararchitects.io/ 23 | -------------------------------------------------------------------------------- /apps/demo/e2e/conditional.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('conditional', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto(''); 6 | await page.getByRole('link', { name: 'withConditional' }).click(); 7 | }); 8 | 9 | test(`uses real user`, async ({ page }) => { 10 | await page.getByRole('radio', { name: 'Real User' }).click(); 11 | await page.getByRole('button', { name: 'Toggle User Component' }).click(); 12 | 13 | await expect(page.getByText('Current User Konrad')).toBeVisible(); 14 | }); 15 | 16 | test(`uses fake user`, async ({ page }) => { 17 | await page.getByRole('radio', { name: 'Fake User' }).click(); 18 | await page.getByRole('button', { name: 'Toggle User Component' }).click(); 19 | 20 | await expect(page.getByText('Current User Tommy Fake')).toBeVisible(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/demo/e2e/devtools.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { Action } from '@ngrx/store'; 3 | 4 | test.describe('DevTools', () => { 5 | test('DevTools do not throw an error when not available', async ({ 6 | page, 7 | }) => { 8 | await page.goto(''); 9 | const errors = []; 10 | page.on('pageerror', (error) => errors.push(error)); 11 | await page.getByRole('link', { name: 'DevTools' }).click(); 12 | await expect( 13 | page.getByRole('row', { name: 'Go for a walk' }) 14 | ).toBeVisible(); 15 | }); 16 | 17 | test('DevTools are syncing state changes', async ({ page }) => { 18 | await page.goto(''); 19 | 20 | await page.evaluate(() => { 21 | window['devtoolsSpy'] = []; 22 | 23 | window['__REDUX_DEVTOOLS_EXTENSION__'] = { 24 | connect: () => { 25 | return { 26 | send: (data: Action) => { 27 | window['devtoolsSpy'].push(data); 28 | }, 29 | }; 30 | }, 31 | }; 32 | }); 33 | await page.getByRole('link', { name: 'DevTools' }).click(); 34 | await page 35 | .getByRole('row', { name: 'Go for a walk' }) 36 | .getByRole('checkbox') 37 | .click(); 38 | await page 39 | .getByRole('row', { name: 'Exercise' }) 40 | .getByRole('checkbox') 41 | .click(); 42 | 43 | await expect( 44 | page.getByRole('region', { name: 'Go for a walk' }) 45 | ).toBeVisible(); 46 | await expect(page.getByRole('region', { name: 'Exercise' })).toBeVisible(); 47 | 48 | await page 49 | .getByRole('row', { name: 'Go for a walk' }) 50 | .getByRole('checkbox') 51 | .click(); 52 | await page 53 | .getByRole('row', { name: 'Exercise' }) 54 | .getByRole('checkbox') 55 | .click(); 56 | 57 | await expect( 58 | page.getByRole('region', { name: 'Go for a walk' }) 59 | ).toBeHidden(); 60 | await expect(page.getByRole('region', { name: 'Exercise' })).toBeHidden(); 61 | 62 | const devtoolsActions = await page.evaluate(() => window['devtoolsSpy']); 63 | 64 | expect(devtoolsActions).toEqual([ 65 | { 66 | type: 'add todo', 67 | }, 68 | { 69 | type: 'select todo 1', 70 | }, 71 | { 72 | type: 'Store Update', 73 | }, 74 | { 75 | type: 'Store Update', 76 | }, 77 | { 78 | type: 'Store Update', 79 | }, 80 | { 81 | type: 'select todo 4', 82 | }, 83 | { 84 | type: 'Store Update', 85 | }, 86 | { 87 | type: 'Store Update', 88 | }, 89 | { 90 | type: 'Store Update', 91 | }, 92 | { 93 | type: 'select todo 1', 94 | }, 95 | { 96 | type: 'Store Update', 97 | }, 98 | { 99 | type: 'Store Update', 100 | }, 101 | { 102 | type: 'select todo 4', 103 | }, 104 | { 105 | type: 'Store Update', 106 | }, 107 | { 108 | type: 'Store Update', 109 | }, 110 | ]); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /apps/demo/e2e/feature-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('feature factory', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto(''); 6 | await page.getByRole('link', { name: 'withFeatureFactory' }).click(); 7 | }); 8 | 9 | test(`loads user`, async ({ page }) => { 10 | await expect(page.getByText('Current User: -')).toBeVisible(); 11 | await page.getByRole('button', { name: 'Load User' }).click(); 12 | await expect(page.getByText('Current User: Konrad')).toBeVisible(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/demo/e2e/immutable-state.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('immutable state', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto(''); 6 | await page.getByRole('link', { name: 'withImmutableState' }).click(); 7 | }); 8 | 9 | for (const position of ['inside', 'outside']) { 10 | test(`mutation ${position}`, async ({ page }) => { 11 | const errorInConsole = page.waitForEvent('console'); 12 | await page.getByRole('button', { name: position }).click(); 13 | expect((await errorInConsole).text()).toContain( 14 | `Cannot assign to read only property 'id'` 15 | ); 16 | }); 17 | } 18 | 19 | test(`mutation via form field`, async ({ page }) => { 20 | const errorInConsole = page.waitForEvent('console'); 21 | await page.getByRole('textbox').focus(); 22 | await page.keyboard.press('Space'); 23 | expect((await errorInConsole).text()).toContain( 24 | `Cannot assign to read only property 'name'` 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /apps/demo/e2e/reset.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto(''); 5 | await page.getByRole('link', { name: 'reset' }).click(); 6 | await page 7 | .getByRole('row', { name: 'Go for a walk' }) 8 | .getByRole('checkbox') 9 | .click(); 10 | await page 11 | .getByRole('row', { name: 'Exercise' }) 12 | .getByRole('checkbox') 13 | .click(); 14 | 15 | await expect( 16 | page.getByRole('row', { name: 'Go for a walk' }).getByRole('checkbox') 17 | ).toBeChecked(); 18 | await expect( 19 | page.getByRole('row', { name: 'Exercise' }).getByRole('checkbox') 20 | ).toBeChecked(); 21 | 22 | await page.getByRole('button', { name: 'Reset State' }).click(); 23 | 24 | await expect( 25 | page.getByRole('row', { name: 'Go for a walk' }).getByRole('checkbox') 26 | ).not.toBeChecked(); 27 | await expect( 28 | page.getByRole('row', { name: 'Exercise' }).getByRole('checkbox') 29 | ).not.toBeChecked(); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/demo/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const playwright = require('eslint-plugin-playwright'); 2 | const nx = require('@nx/eslint-plugin'); 3 | const baseConfig = require('../../eslint.config.cjs'); 4 | 5 | module.exports = [ 6 | playwright.configs['flat/recommended'], 7 | 8 | ...baseConfig, 9 | ...nx.configs['flat/angular'], 10 | ...nx.configs['flat/angular-template'], 11 | { 12 | files: ['**/*.ts'], 13 | rules: { 14 | '@angular-eslint/directive-selector': [ 15 | 'error', 16 | { 17 | type: 'attribute', 18 | prefix: 'demo', 19 | style: 'camelCase', 20 | }, 21 | ], 22 | '@angular-eslint/component-selector': [ 23 | 'error', 24 | { 25 | type: 'element', 26 | prefix: 'demo', 27 | style: 'kebab-case', 28 | }, 29 | ], 30 | }, 31 | }, 32 | { 33 | files: ['**/*.ts', '**/*.js'], 34 | // Override or add rules here 35 | rules: {}, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /apps/demo/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | displayName: 'demo', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | coverageDirectory: '../../coverage/apps/demo', 6 | transform: { 7 | '^.+\\.(ts|mjs|js|html)$': [ 8 | 'jest-preset-angular', 9 | { 10 | tsconfig: '/tsconfig.spec.json', 11 | stringifyContentPathRegex: '\\.(html|svg)$', 12 | }, 13 | ], 14 | }, 15 | testPathIgnorePatterns: ['e2e'], 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/demo/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | import * as path from 'node:path'; 3 | 4 | // For CI, you may want to set BASE_URL to the deployed application. 5 | const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; 6 | 7 | /** 8 | * Read environment variables from file. 9 | * https://github.com/motdotla/dotenv 10 | */ 11 | // require('dotenv').config(); 12 | 13 | /** 14 | * See https://playwright.dev/docs/test-configuration. 15 | */ 16 | export default defineConfig({ 17 | use: { 18 | baseURL, 19 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 20 | trace: 'on-first-retry', 21 | }, 22 | /* Run your local dev server before starting the tests */ 23 | webServer: { 24 | command: 'pnpm start', 25 | url: 'http://localhost:4200', 26 | reuseExistingServer: true, 27 | cwd: path.join(__dirname, '../..'), 28 | }, 29 | projects: [ 30 | { 31 | name: 'chromium', 32 | use: { ...devices['Desktop Chrome'] }, 33 | }, 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /apps/demo/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "prefix": "ngrx-toolkit", 6 | "sourceRoot": "apps/demo/src", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@angular-devkit/build-angular:application", 11 | "outputs": ["{options.outputPath}"], 12 | "options": { 13 | "outputPath": "dist/apps/demo", 14 | "index": "apps/demo/src/index.html", 15 | "browser": "apps/demo/src/main.ts", 16 | "polyfills": ["zone.js"], 17 | "tsConfig": "apps/demo/tsconfig.app.json", 18 | "assets": ["apps/demo/src/favicon.ico", "apps/demo/src/assets"], 19 | "styles": [ 20 | "@angular/material/prebuilt-themes/deeppurple-amber.css", 21 | "apps/demo/src/styles.css" 22 | ], 23 | "scripts": [] 24 | }, 25 | "configurations": { 26 | "production": { 27 | "budgets": [ 28 | { 29 | "type": "initial", 30 | "maximumWarning": "500kb", 31 | "maximumError": "2mb" 32 | }, 33 | { 34 | "type": "anyComponentStyle", 35 | "maximumWarning": "2kb", 36 | "maximumError": "4kb" 37 | } 38 | ], 39 | "outputHashing": "all" 40 | }, 41 | "development": { 42 | "optimization": false, 43 | "extractLicenses": false, 44 | "sourceMap": true 45 | } 46 | }, 47 | "defaultConfiguration": "production" 48 | }, 49 | "serve": { 50 | "executor": "@angular-devkit/build-angular:dev-server", 51 | "configurations": { 52 | "production": { 53 | "buildTarget": "demo:build:production" 54 | }, 55 | "development": { 56 | "buildTarget": "demo:build:development" 57 | } 58 | }, 59 | "defaultConfiguration": "development" 60 | }, 61 | "extract-i18n": { 62 | "executor": "@angular-devkit/build-angular:extract-i18n", 63 | "options": { 64 | "buildTarget": "demo:build" 65 | } 66 | }, 67 | "lint": { 68 | "executor": "@nx/eslint:lint", 69 | "outputs": ["{options.outputFile}"] 70 | }, 71 | "test": { 72 | "executor": "@nx/jest:jest", 73 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 74 | "options": { 75 | "jestConfig": "apps/demo/jest.config.ts" 76 | } 77 | }, 78 | "e2e": { 79 | "executor": "@nx/playwright:playwright", 80 | "outputs": ["{workspaceRoot}/dist/.playwright/apps/demo"], 81 | "options": { 82 | "config": "apps/demo/playwright.config.ts" 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | NgRx Toolkit Demo 3 | 4 | 5 | 6 | 7 | DevTools 8 | withRedux 9 | withDataService (Simple) 12 | withDataService (Dynamic) 15 | withPagination 18 | Redux Connector 21 | withStorageSync 22 | withStorageSync(IndexedDB) 25 | withReset 26 | withImmutableState 27 | withFeatureFactory 28 | withConditional 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatTableModule } from '@angular/material/table'; 3 | import { MatCheckboxModule } from '@angular/material/checkbox'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { RouterLink, RouterOutlet } from '@angular/router'; 6 | import { CommonModule } from '@angular/common'; 7 | import { MatToolbarModule } from '@angular/material/toolbar'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { 10 | MatDrawer, 11 | MatDrawerContainer, 12 | MatDrawerContent, 13 | } from '@angular/material/sidenav'; 14 | 15 | @Component({ 16 | selector: 'demo-root', 17 | templateUrl: './app.component.html', 18 | imports: [ 19 | MatTableModule, 20 | MatCheckboxModule, 21 | MatIconModule, 22 | MatListModule, 23 | RouterLink, 24 | RouterOutlet, 25 | CommonModule, 26 | MatToolbarModule, 27 | MatDrawer, 28 | MatDrawerContainer, 29 | MatDrawerContent, 30 | ], 31 | styles: ` 32 | .container { 33 | display: inline; 34 | } 35 | .content { 36 | margin: 4em; 37 | }`, 38 | }) 39 | export class AppComponent {} 40 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideRouter, withComponentInputBinding } from '@angular/router'; 3 | import { appRoutes } from './app.routes'; 4 | import { provideAnimations } from '@angular/platform-browser/animations'; 5 | import { provideHttpClient } from '@angular/common/http'; 6 | import { LayoutModule } from '@angular/cdk/layout'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | provideRouter(appRoutes, withComponentInputBinding()), 11 | provideAnimations(), 12 | provideHttpClient(), 13 | importProvidersFrom(LayoutModule), 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | 3 | export const appRoutes: Route[] = [ 4 | { 5 | path: '', 6 | loadChildren: () => import('./lazy-routes').then((m) => m.lazyRoutes), 7 | }, 8 | ]; 9 | -------------------------------------------------------------------------------- /apps/demo/src/app/category.store.ts: -------------------------------------------------------------------------------- 1 | import { patchState, signalStore, withHooks } from '@ngrx/signals'; 2 | import { setAllEntities, withEntities } from '@ngrx/signals/entities'; 3 | import { withDevtools } from '@angular-architects/ngrx-toolkit'; 4 | 5 | export interface Category { 6 | id: number; 7 | name: string; 8 | } 9 | 10 | export const CategoryStore = signalStore( 11 | { providedIn: 'root' }, 12 | withDevtools('category'), 13 | withEntities(), 14 | withHooks({ 15 | onInit: (store) => { 16 | patchState( 17 | store, 18 | setAllEntities([ 19 | { id: 1, name: 'Important' }, 20 | { id: 2, name: 'Nice to Have' }, 21 | ]) 22 | ); 23 | }, 24 | }) 25 | ); 26 | -------------------------------------------------------------------------------- /apps/demo/src/app/core/sidebar/sidebar.component.css: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav { 6 | width: 300px; 7 | } 8 | 9 | .sidenav .mat-toolbar { 10 | background: inherit; 11 | } 12 | 13 | .mat-toolbar.mat-primary { 14 | position: sticky; 15 | top: 0; 16 | z-index: 1; 17 | } 18 | 19 | .app-container { 20 | padding: 20px; 21 | } -------------------------------------------------------------------------------- /apps/demo/src/app/core/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | Menu 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/demo/src/app/core/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Component, Inject } from '@angular/core'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { MatListModule } from '@angular/material/list'; 7 | import { MatSidenavModule } from '@angular/material/sidenav'; 8 | import { MatToolbarModule } from '@angular/material/toolbar'; 9 | import { RouterModule } from '@angular/router'; 10 | import { map, shareReplay } from 'rxjs'; 11 | 12 | @Component({ 13 | selector: 'demo-sidebar-cmp', 14 | imports: [ 15 | RouterModule, 16 | CommonModule, 17 | MatToolbarModule, 18 | MatButtonModule, 19 | MatSidenavModule, 20 | MatIconModule, 21 | MatListModule, 22 | ], 23 | templateUrl: './sidebar.component.html', 24 | styleUrls: ['./sidebar.component.css'] 25 | }) 26 | export class SidebarComponent { 27 | isHandset$ = this.breakpointObserver.observe(Breakpoints.Handset) 28 | .pipe( 29 | map(result => result.matches), 30 | shareReplay() 31 | ); 32 | 33 | constructor( 34 | @Inject(BreakpointObserver) private breakpointObserver: BreakpointObserver) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/demo/src/app/devtools/todo-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject, input } from '@angular/core'; 2 | import { MatCardModule } from '@angular/material/card'; 3 | import { patchState, signalStore, withHooks, withState } from '@ngrx/signals'; 4 | import { 5 | renameDevtoolsName, 6 | withDevtools, 7 | withGlitchTracking, 8 | withMapper, 9 | } from '@angular-architects/ngrx-toolkit'; 10 | import { Todo } from '../shared/todo.service'; 11 | 12 | /** 13 | * This Store can be instantiated multiple times, if the user 14 | * selects different todos. 15 | * 16 | * The devtools extension will start to index the store names. 17 | * 18 | * Since we want to apply our own store name, (depending on the id), we 19 | * run renameDevtoolsStore() in the effect. 20 | */ 21 | const TodoDetailStore = signalStore( 22 | withDevtools( 23 | 'todo-detail', 24 | withMapper((state: Record) => { 25 | return Object.keys(state).reduce((acc, key) => { 26 | if (key === 'secret') { 27 | return acc; 28 | } 29 | acc[key] = state[key]; 30 | 31 | return acc; 32 | }, {} as Record); 33 | }), 34 | withGlitchTracking() 35 | ), 36 | withState({ 37 | id: 1, 38 | secret: 'do not show in DevTools', 39 | active: false, 40 | }), 41 | withHooks((store) => ({ onInit: () => patchState(store, { active: true }) })) 42 | ); 43 | 44 | @Component({ 45 | selector: 'demo-todo-detail', 46 | template: `
47 | 48 | {{ todo().name }} 49 | 50 | 51 | 52 | 53 |
`, 54 | imports: [MatCardModule], 55 | providers: [TodoDetailStore], 56 | styles: ` 57 | mat-card { 58 | margin: 10px; 59 | } 60 | `, 61 | }) 62 | export class TodoDetailComponent { 63 | readonly #todoDetailStore = inject(TodoDetailStore); 64 | todo = input.required(); 65 | 66 | constructor() { 67 | effect(() => { 68 | renameDevtoolsName(this.#todoDetailStore, `todo-${this.todo().id}`); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/demo/src/app/devtools/todo-store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | signalStore, 3 | withComputed, 4 | withHooks, 5 | withMethods, 6 | withState, 7 | } from '@ngrx/signals'; 8 | import { 9 | removeEntity, 10 | setEntity, 11 | updateEntity, 12 | withEntities, 13 | } from '@ngrx/signals/entities'; 14 | import { updateState, withDevtools } from '@angular-architects/ngrx-toolkit'; 15 | import { computed, inject } from '@angular/core'; 16 | import { Todo, AddTodo, TodoService } from '../shared/todo.service'; 17 | 18 | export const TodoStore = signalStore( 19 | { providedIn: 'root' }, 20 | withDevtools('todo-store'), 21 | withEntities(), 22 | withState({ 23 | selectedIds: [] as number[], 24 | }), 25 | withMethods((store) => { 26 | let currentId = 0; 27 | return { 28 | add(todo: AddTodo) { 29 | updateState(store, 'add todo', setEntity({ id: ++currentId, ...todo })); 30 | }, 31 | 32 | remove(id: number) { 33 | updateState(store, 'remove todo', removeEntity(id)); 34 | }, 35 | 36 | toggleFinished(id: number): void { 37 | const todo = store.entityMap()[id]; 38 | updateState( 39 | store, 40 | 'toggle todo', 41 | updateEntity({ id, changes: { finished: !todo.finished } }) 42 | ); 43 | }, 44 | toggleSelectTodo(id: number) { 45 | updateState(store, `select todo ${id}`, ({ selectedIds }) => { 46 | if (selectedIds.includes(id)) { 47 | return { 48 | selectedIds: selectedIds.filter( 49 | (selectedId) => selectedId !== id 50 | ), 51 | }; 52 | } 53 | return { 54 | selectedIds: [...store.selectedIds(), id], 55 | }; 56 | }); 57 | }, 58 | }; 59 | }), 60 | withComputed((state) => ({ 61 | selectedTodos: computed(() => 62 | state.selectedIds().map((id) => state.entityMap()[id]) 63 | ), 64 | })), 65 | withHooks({ 66 | onInit: (store, todoService = inject(TodoService)) => { 67 | const todos = todoService.getData(); 68 | todos.forEach((todo) => store.add(todo)); 69 | }, 70 | }) 71 | ); 72 | -------------------------------------------------------------------------------- /apps/demo/src/app/devtools/todo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject } from '@angular/core'; 2 | import { MatCheckboxModule } from '@angular/material/checkbox'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { MatTableDataSource, MatTableModule } from '@angular/material/table'; 5 | import { SelectionModel } from '@angular/cdk/collections'; 6 | import { TodoStore } from './todo-store'; 7 | import { TodoDetailComponent } from './todo-detail.component'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { Todo } from '../shared/todo.service'; 10 | 11 | @Component({ 12 | selector: 'demo-todo', 13 | template: ` 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | delete 26 | 27 | 28 | 29 | 30 | 31 | Name 32 | {{ element.name }} 33 | 34 | 35 | 36 | 37 | Deadline 39 | 40 | {{ element.deadline }} 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 |
53 | @for (todo of todoStore.selectedTodos(); track todo) { 54 | 55 | } 56 |
57 | `, 58 | styles: `.actions { 59 | display: flex; 60 | align-items: center; 61 | } 62 | 63 | .details { 64 | margin: 20px; 65 | display: flex; 66 | } 67 | `, 68 | imports: [ 69 | MatCheckboxModule, 70 | MatIconModule, 71 | MatTableModule, 72 | TodoDetailComponent, 73 | FormsModule, 74 | ], 75 | }) 76 | export class TodoComponent { 77 | todoStore = inject(TodoStore); 78 | 79 | displayedColumns: string[] = ['finished', 'name', 'deadline']; 80 | dataSource = new MatTableDataSource([]); 81 | selection = new SelectionModel(true, []); 82 | 83 | constructor() { 84 | effect(() => { 85 | this.dataSource.data = this.todoStore.entities(); 86 | }); 87 | } 88 | 89 | checkboxLabel(todo: Todo) { 90 | this.todoStore.toggleSelectTodo(todo.id); 91 | } 92 | 93 | removeTodo(todo: Todo) { 94 | this.todoStore.remove(todo.id); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /apps/demo/src/app/feature-factory/feature-factory.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { 3 | patchState, 4 | signalStore, 5 | signalStoreFeature, 6 | withMethods, 7 | withState, 8 | } from '@ngrx/signals'; 9 | import { MatButton } from '@angular/material/button'; 10 | import { FormsModule } from '@angular/forms'; 11 | import { lastValueFrom, of } from 'rxjs'; 12 | import { withFeatureFactory } from '@angular-architects/ngrx-toolkit'; 13 | 14 | type User = { 15 | id: number; 16 | name: string; 17 | }; 18 | 19 | function withMyEntity(loadMethod: (id: number) => Promise) { 20 | return signalStoreFeature( 21 | withState({ 22 | currentId: 1 as number | undefined, 23 | entity: undefined as undefined | Entity, 24 | }), 25 | withMethods((store) => ({ 26 | async load(id: number) { 27 | const entity = await loadMethod(1); 28 | patchState(store, { entity, currentId: id }); 29 | }, 30 | })) 31 | ); 32 | } 33 | 34 | const UserStore = signalStore( 35 | { providedIn: 'root' }, 36 | withMethods(() => ({ 37 | findById(id: number) { 38 | return of({ id: 1, name: 'Konrad' }); 39 | }, 40 | })), 41 | withFeatureFactory((store) => { 42 | const loader = (id: number) => lastValueFrom(store.findById(id)); 43 | return withMyEntity(loader); 44 | }) 45 | ); 46 | 47 | @Component({ 48 | template: ` 49 |

50 |
withFeatureFactory
51 |

52 | 53 | 54 | 55 |

Current User: {{ userStore.entity()?.name || '-' }}

56 | `, 57 | imports: [MatButton, FormsModule], 58 | }) 59 | export class FeatureFactoryComponent { 60 | protected readonly userStore = inject(UserStore); 61 | 62 | loadUser() { 63 | void this.userStore.load(1); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-dynamic/flight-booking.store.ts: -------------------------------------------------------------------------------- 1 | import { FlightService } from '../shared/flight.service'; 2 | 3 | import { signalStore, type } from '@ngrx/signals'; 4 | 5 | import { withEntities } from '@ngrx/signals/entities'; 6 | import { 7 | withCallState, 8 | withDataService, 9 | withUndoRedo, 10 | } from '@angular-architects/ngrx-toolkit'; 11 | import { Flight } from '../shared/flight'; 12 | 13 | export const FlightBookingStore = signalStore( 14 | { providedIn: 'root' }, 15 | withCallState({ 16 | collection: 'flight', 17 | }), 18 | withEntities({ 19 | entity: type(), 20 | collection: 'flight', 21 | }), 22 | withDataService({ 23 | dataServiceType: FlightService, 24 | filter: { from: 'Paris', to: 'New York' }, 25 | collection: 'flight', 26 | }), 27 | withUndoRedo({ 28 | collections: ['flight'], 29 | }) 30 | ); 31 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-dynamic/flight-edit.component.html: -------------------------------------------------------------------------------- 1 |

Flight Edit (Dynamic)

2 | 3 | @if(loading()) { 4 |
Loading ...
5 | } 6 | 7 | @if(error()) { 8 |

Error: {{error()}}

9 | } 10 | 11 | @if(current()) { 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | } -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-dynamic/flight-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, OnInit, ViewChild, inject } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FormsModule, NgForm } from '@angular/forms'; 5 | import { FlightBookingStore } from './flight-booking.store'; 6 | import { Flight } from '../shared/flight'; 7 | 8 | @Component({ 9 | imports: [CommonModule, RouterModule, FormsModule], 10 | selector: 'demo-flight-edit', 11 | templateUrl: './flight-edit.component.html', 12 | }) 13 | export class FlightEditDynamicComponent implements OnInit { 14 | 15 | @ViewChild(NgForm) 16 | private form!: NgForm; 17 | 18 | private store = inject(FlightBookingStore); 19 | 20 | current = this.store.currentFlight; 21 | loading = this.store.flightLoading; 22 | error = this.store.flightError; 23 | 24 | @Input({ required: true }) 25 | id = ''; 26 | 27 | ngOnInit(): void { 28 | this.store.loadFlightById(this.id); 29 | } 30 | 31 | async save() { 32 | const flight = this.form.value as Flight; 33 | if (flight.id) { 34 | await this.store.updateFlight(flight); 35 | } 36 | else { 37 | await this.store.createFlight(flight); 38 | } 39 | } 40 | 41 | async createNew() { 42 | await this.store.setCurrentFlight({} as Flight); 43 | } 44 | 45 | async deleteFlight() { 46 | await this.store.deleteFlight(this.form.value) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-dynamic/flight-search.component.html: -------------------------------------------------------------------------------- 1 |

Flight Search (Dynamic)

2 | 3 |
4 |
5 | 15 |
16 | 17 |
18 | 28 |
29 | 30 |
31 | 38 | 39 | 42 | 43 | 46 |
47 |
48 | 49 |
50 | Loading ... 51 |
52 | 53 |
54 |
58 | 63 | Edit 66 | 67 |
68 |
69 | 70 |
{{ selected() | json }}
71 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-dynamic/flight-search.component.ts: -------------------------------------------------------------------------------- 1 | import { JsonPipe, NgForOf, NgIf } from '@angular/common'; 2 | import { Component, inject } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { FlightCardComponent } from '../shared/flight-card.component'; 5 | import { RouterLink } from '@angular/router'; 6 | import { FlightBookingStore } from './flight-booking.store'; 7 | 8 | @Component({ 9 | imports: [ 10 | NgIf, 11 | NgForOf, 12 | JsonPipe, 13 | FormsModule, 14 | FlightCardComponent, 15 | RouterLink, 16 | ], 17 | selector: 'demo-flight-search', 18 | templateUrl: './flight-search.component.html', 19 | }) 20 | export class FlightSearchDynamicComponent { 21 | private store = inject(FlightBookingStore); 22 | 23 | from = this.store.flightFilter.from; 24 | to = this.store.flightFilter.to; 25 | flights = this.store.flightEntities; 26 | selected = this.store.selectedFlightEntities; 27 | selectedIds = this.store.selectedFlightIds; 28 | 29 | loading = this.store.flightLoading; 30 | 31 | canUndo = this.store.canUndo; 32 | canRedo = this.store.canRedo; 33 | 34 | async search() { 35 | this.store.loadFlightEntities(); 36 | } 37 | 38 | undo(): void { 39 | this.store.undo(); 40 | } 41 | 42 | redo(): void { 43 | this.store.redo(); 44 | } 45 | 46 | updateCriteria(from: string, to: string): void { 47 | this.store.updateFlightFilter({ from, to }); 48 | } 49 | 50 | updateBasket(id: number, selected: boolean): void { 51 | this.store.updateSelectedFlightEntities(id, selected); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-simple/flight-booking-simple.store.ts: -------------------------------------------------------------------------------- 1 | import { FlightService } from '../shared/flight.service'; 2 | 3 | import { signalStore } from '@ngrx/signals'; 4 | 5 | import { withEntities } from '@ngrx/signals/entities'; 6 | import { 7 | withCallState, 8 | withDataService, 9 | withUndoRedo, 10 | } from '@angular-architects/ngrx-toolkit'; 11 | import { Flight } from '../shared/flight'; 12 | 13 | export const SimpleFlightBookingStore = signalStore( 14 | { providedIn: 'root' }, 15 | withCallState(), 16 | withEntities(), 17 | withDataService({ 18 | dataServiceType: FlightService, 19 | filter: { from: 'Paris', to: 'New York' }, 20 | }), 21 | withUndoRedo() 22 | ); 23 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-simple/flight-edit-simple.component.html: -------------------------------------------------------------------------------- 1 |

Flight Edit (Simple)

2 | 3 | @if(loading()) { 4 |
One moment please ...
5 | } 6 | 7 | @if(error()) { 8 |

Error: {{error()}}

9 | } 10 | 11 | @if(current()) { 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | } -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-simple/flight-edit-simple.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, OnInit, ViewChild, inject } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FormsModule, NgForm } from '@angular/forms'; 5 | import { SimpleFlightBookingStore } from './flight-booking-simple.store'; 6 | import { Flight } from '../shared/flight'; 7 | 8 | @Component({ 9 | imports: [CommonModule, RouterModule, FormsModule], 10 | selector: 'demo-flight-edit-simple', 11 | templateUrl: './flight-edit-simple.component.html', 12 | }) 13 | export class FlightEditSimpleComponent implements OnInit { 14 | 15 | @ViewChild(NgForm) 16 | private form!: NgForm; 17 | 18 | private store = inject(SimpleFlightBookingStore); 19 | 20 | current = this.store.current; 21 | loading = this.store.loading; 22 | error = this.store.error; 23 | 24 | @Input({ required: true }) 25 | id = ''; 26 | 27 | ngOnInit(): void { 28 | this.store.loadById(this.id); 29 | } 30 | 31 | async save() { 32 | const flight = this.form.value as Flight; 33 | if (flight.id) { 34 | await this.store.update(flight); 35 | } 36 | else { 37 | await this.store.create(flight); 38 | } 39 | } 40 | 41 | async createNew() { 42 | await this.store.setCurrent({} as Flight); 43 | } 44 | 45 | async deleteFlight() { 46 | await this.store.delete(this.form.value) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-simple/flight-search-simple.component.html: -------------------------------------------------------------------------------- 1 |

Flight Search (Simple)

2 | 3 |
4 |
5 | 15 |
16 | 17 |
21 | Invalid city! 22 |
23 | 24 |
25 | 34 |
35 | 36 |
37 | 44 | 45 | 48 | 51 |
52 |
53 | 54 |
55 | Loading ... 56 |
57 | 58 |
59 |
63 | 68 | Edit 71 | 72 |
73 |
74 | 75 |
{{ selected() | json }}
76 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-data-service-simple/flight-search-simple.component.ts: -------------------------------------------------------------------------------- 1 | import { JsonPipe, NgForOf, NgIf } from '@angular/common'; 2 | import { Component, inject } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ChangeDetectionStrategy } from '@angular/core'; 5 | import { RouterLink } from '@angular/router'; 6 | import { FlightCardComponent } from '../shared/flight-card.component'; 7 | import { SimpleFlightBookingStore } from './flight-booking-simple.store'; 8 | import { MatTableModule } from '@angular/material/table'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | 12 | @Component({ 13 | imports: [ 14 | NgIf, 15 | NgForOf, 16 | JsonPipe, 17 | FormsModule, 18 | FlightCardComponent, 19 | MatTableModule, 20 | MatInputModule, 21 | MatButtonModule, 22 | RouterLink, 23 | ], 24 | selector: 'demo-flight-search', 25 | templateUrl: './flight-search-simple.component.html', 26 | changeDetection: ChangeDetectionStrategy.OnPush, 27 | }) 28 | export class FlightSearchSimpleComponent { 29 | private store = inject(SimpleFlightBookingStore); 30 | 31 | from = this.store.filter.from; 32 | to = this.store.filter.to; 33 | flights = this.store.entities; 34 | selected = this.store.selectedEntities; 35 | selectedIds = this.store.selectedIds; 36 | 37 | loading = this.store.loading; 38 | 39 | canUndo = this.store.canUndo; 40 | canRedo = this.store.canRedo; 41 | 42 | async search() { 43 | this.store.load(); 44 | } 45 | 46 | undo(): void { 47 | this.store.undo(); 48 | } 49 | 50 | redo(): void { 51 | this.store.redo(); 52 | } 53 | 54 | updateCriteria(from: string, to: string): void { 55 | this.store.updateFilter({ from, to }); 56 | } 57 | 58 | updateBasket(id: number, selected: boolean): void { 59 | this.store.updateSelected(id, selected); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-redux-connector/+state/actions.ts: -------------------------------------------------------------------------------- 1 | import { createActionGroup, emptyProps, props } from "@ngrx/store"; 2 | import { FlightFilter } from "../../shared/flight.service"; 3 | import { Flight } from "../../shared/flight"; 4 | 5 | 6 | export const ticketActions = createActionGroup({ 7 | source: 'tickets', 8 | events: { 9 | 'flights load': props(), 10 | 'flights loaded': props<{ flights: Flight[] }>(), 11 | 'flights loaded by passenger': props<{ flights: Flight[] }>(), 12 | 'flight update': props<{ flight: Flight }>(), 13 | 'flights clear': emptyProps() 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-redux-connector/+state/model.ts: -------------------------------------------------------------------------------- 1 | import { Flight } from "../../shared/flight"; 2 | 3 | 4 | export type FlightState = { 5 | flights: Flight[]; 6 | basket: unknown; 7 | tickets: unknown; 8 | hide: number[]; 9 | }; 10 | 11 | export const initialTicketState: FlightState = { 12 | flights: [], 13 | basket: {}, 14 | tickets: {}, 15 | hide: [3, 5] 16 | }; 17 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-redux-connector/+state/redux.ts: -------------------------------------------------------------------------------- 1 | import { ticketActions } from './actions'; 2 | import { FlightStore } from './store'; 3 | import { 4 | createReduxState, 5 | withActionMappers, 6 | mapAction, 7 | } from '@angular-architects/ngrx-toolkit/redux-connector'; 8 | 9 | export const { provideFlightStore, injectFlightStore } = 10 | /** 11 | * Redux 12 | * - Provider 13 | * - Injectable Store 14 | * - Action to Method Mapper 15 | * - Selector Signals 16 | * - Dispatch 17 | */ 18 | createReduxState('flight', FlightStore, (store) => 19 | withActionMappers( 20 | mapAction( 21 | ticketActions.flightsLoad, 22 | store.loadFlights, 23 | ticketActions.flightsLoaded 24 | ), 25 | mapAction( 26 | ticketActions.flightsLoaded, 27 | ticketActions.flightsLoadedByPassenger, 28 | store.setFlights 29 | ), 30 | mapAction(ticketActions.flightUpdate, store.updateFlight), 31 | mapAction(ticketActions.flightsClear, store.clearFlights) 32 | ) 33 | ); 34 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-redux-connector/+state/store.ts: -------------------------------------------------------------------------------- 1 | import { computed, inject } from '@angular/core'; 2 | import { 3 | patchState, 4 | signalStore, 5 | type, 6 | withComputed, 7 | withMethods, 8 | } from '@ngrx/signals'; 9 | import { 10 | removeAllEntities, 11 | setAllEntities, 12 | updateEntity, 13 | withEntities, 14 | } from '@ngrx/signals/entities'; 15 | import { reduxMethod } from '@angular-architects/ngrx-toolkit/redux-connector'; 16 | import { from, map, pipe, switchMap } from 'rxjs'; 17 | import { Flight } from '../../shared/flight'; 18 | import { FlightFilter, FlightService } from '../../shared/flight.service'; 19 | 20 | export const FlightStore = signalStore( 21 | { providedIn: 'root' }, 22 | // State 23 | withEntities({ entity: type(), collection: 'flight' }), 24 | withEntities({ entity: type(), collection: 'hide' }), 25 | // Selectors 26 | withComputed(({ flightEntities, hideEntities }) => ({ 27 | filteredFlights: computed(() => 28 | flightEntities().filter((flight) => !hideEntities().includes(flight.id)) 29 | ), 30 | flightCount: computed(() => flightEntities().length), 31 | })), 32 | // Updater 33 | withMethods((store) => ({ 34 | setFlights: (state: { flights: Flight[] }) => 35 | patchState( 36 | store, 37 | setAllEntities(state.flights, { collection: 'flight' }) 38 | ), 39 | updateFlight: (state: { flight: Flight }) => 40 | patchState( 41 | store, 42 | updateEntity( 43 | { id: state.flight.id, changes: state.flight }, 44 | { collection: 'flight' } 45 | ) 46 | ), 47 | clearFlights: () => 48 | patchState(store, removeAllEntities({ collection: 'flight' })), 49 | })), 50 | // Effects 51 | withMethods((store, flightService = inject(FlightService)) => ({ 52 | loadFlights: reduxMethod( 53 | pipe( 54 | switchMap((filter) => 55 | from(flightService.load({ from: filter.from, to: filter.to })) 56 | ), 57 | map((flights) => ({ flights })) 58 | ), 59 | store.setFlights 60 | ), 61 | })) 62 | ); 63 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-redux-connector/flight-search.component.html: -------------------------------------------------------------------------------- 1 |

Flight Search (Redux Connector)

2 | 3 |
4 |
5 | 14 |
15 | 16 |
17 | 26 |
27 | 28 |
29 | 36 | 37 | @if (flights().length) { 38 | 39 | } 40 |
41 |
42 | 43 |
44 | @for (flight of flights(); track flight.id) { 45 |
46 | 51 | 52 |
53 | } 54 |
55 | 56 |
{{ localState.basket() | json }}
57 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-redux-connector/flight-search.component.ts: -------------------------------------------------------------------------------- 1 | import { JsonPipe } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { FlightCardComponent } from '../shared/flight-card.component'; 5 | import { ticketActions } from './+state/actions'; 6 | import { injectFlightStore } from './+state/redux'; 7 | import { patchState, signalState } from '@ngrx/signals'; 8 | import { FlightFilter } from '../shared/flight.service'; 9 | import { Flight } from '../shared/flight'; 10 | 11 | @Component({ 12 | imports: [JsonPipe, FormsModule, FlightCardComponent], 13 | selector: 'demo-flight-search-redux-connector', 14 | templateUrl: './flight-search.component.html', 15 | }) 16 | export class FlightSearchReducConnectorComponent { 17 | private store = injectFlightStore(); 18 | 19 | protected localState = signalState({ 20 | filter: { 21 | from: 'Frankfurt', 22 | to: 'Paris', 23 | }, 24 | basket: { 25 | 888: true, 26 | 889: true, 27 | } as Record, 28 | }); 29 | 30 | protected flights = this.store.flightEntities; 31 | 32 | protected search() { 33 | this.store.dispatch( 34 | ticketActions.flightsLoad({ 35 | from: this.localState.filter.from(), 36 | to: this.localState.filter.to(), 37 | }) 38 | ); 39 | } 40 | 41 | protected patchFilter(filter: Partial) { 42 | patchState(this.localState, (state) => ({ 43 | filter: { 44 | ...state.filter, 45 | ...filter, 46 | }, 47 | })); 48 | } 49 | 50 | protected select(id: number, selected: boolean): void { 51 | patchState(this.localState, (state) => ({ 52 | basket: { 53 | ...state.basket, 54 | [id]: selected, 55 | }, 56 | })); 57 | } 58 | 59 | protected delay(flight: Flight): void { 60 | const oldFlight = flight; 61 | const oldDate = new Date(oldFlight.date); 62 | 63 | const newDate = new Date(oldDate.getTime() + 1000 * 60 * 5); // Add 5 min 64 | const newFlight = { 65 | ...oldFlight, 66 | date: newDate.toISOString(), 67 | delayed: true, 68 | }; 69 | 70 | this.store.dispatch(ticketActions.flightUpdate({ flight: newFlight })); 71 | } 72 | 73 | protected reset(): void { 74 | this.store.dispatch(ticketActions.flightsClear()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-with-pagination/flight-search-with-pagination.component.html: -------------------------------------------------------------------------------- 1 |

Flight Search (Pagination)

2 | 3 |
4 |
5 | 6 | Name 7 | 13 | 14 |
15 | 16 |
17 | 18 | Name 19 | 25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | From 35 | {{ element.from }} 36 | 37 | 38 | 39 | 40 | To 41 | {{ element.to }} 42 | 43 | 44 | 45 | 46 | Date 47 | {{ 48 | element.date | date 49 | }} 50 | 51 | 52 | 53 | 57 | 58 | 67 | 68 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-with-pagination/flight-search-with-pagination.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject } from '@angular/core'; 2 | import { MatTableDataSource, MatTableModule } from '@angular/material/table'; 3 | import { DatePipe } from '@angular/common'; 4 | import { SelectionModel } from '@angular/cdk/collections'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { FlightBookingStore } from './flight-store'; 9 | import { Flight } from '../shared/flight'; 10 | import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; 11 | 12 | @Component({ 13 | selector: 'demo-flight-search-with-pagination', 14 | templateUrl: 'flight-search-with-pagination.component.html', 15 | imports: [ 16 | MatTableModule, 17 | MatPaginatorModule, 18 | DatePipe, 19 | MatInputModule, 20 | FormsModule, 21 | MatButtonModule, 22 | ], 23 | providers: [FlightBookingStore] 24 | }) 25 | export class FlightSearchWithPaginationComponent { 26 | searchParams: { from: string; to: string } = { from: 'Wien', to: '' }; 27 | flightStore = inject(FlightBookingStore); 28 | 29 | displayedColumns: string[] = ['from', 'to', 'date']; 30 | dataSource = new MatTableDataSource([]); 31 | selection = new SelectionModel(true, []); 32 | 33 | constructor() { 34 | effect(() => { 35 | this.dataSource.data = this.flightStore.selectedPageFlightEntities(); 36 | }); 37 | this.flightStore.loadFlightEntities(); 38 | } 39 | 40 | search() { 41 | this.flightStore.updateFlightFilter( 42 | this.searchParams 43 | ); 44 | this.flightStore.loadFlightEntities(); 45 | } 46 | 47 | handlePageEvent(e: PageEvent) { 48 | this.flightStore.setFlightPageSize(e.pageSize); 49 | this.flightStore.gotoFlightPage(e.pageIndex); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search-with-pagination/flight-store.ts: -------------------------------------------------------------------------------- 1 | import { FlightService } from '../shared/flight.service'; 2 | import { patchState, signalStore, type, withMethods } from '@ngrx/signals'; 3 | import { withEntities } from '@ngrx/signals/entities'; 4 | import { 5 | withCallState, 6 | withDataService, 7 | withPagination, 8 | setPageSize, 9 | gotoPage, 10 | } from '@angular-architects/ngrx-toolkit'; 11 | import { Flight } from '../shared/flight'; 12 | 13 | // Name of the collection 14 | const collectionName = 'flight'; 15 | 16 | export const FlightBookingStore = signalStore( 17 | withCallState({ 18 | collection: collectionName, 19 | }), 20 | withEntities({ 21 | entity: type(), 22 | collection: collectionName, 23 | }), 24 | withDataService({ 25 | dataServiceType: FlightService, 26 | filter: { from: 'Wien', to: '' }, 27 | collection: collectionName, 28 | }), 29 | withPagination({ 30 | entity: type(), 31 | collection: collectionName, 32 | }), 33 | withMethods((store) => ({ 34 | setFlightPageSize: (size: number) => { 35 | patchState(store, setPageSize(size, { collection: collectionName })); 36 | }, 37 | gotoFlightPage: (page: number) => { 38 | patchState(store, gotoPage(page, { collection: collectionName })); 39 | }, 40 | })) 41 | ); 42 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search/flight-search.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Name 5 | 6 | 7 |
8 | 9 |
10 | 11 | Name 12 | 13 | 14 |
15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | From 23 | {{ element.from }} 24 | 25 | 26 | 27 | 28 | To 29 | {{ element.to }} 30 | 31 | 32 | 33 | 34 | Date 35 | {{ 36 | element.date | date 37 | }} 38 | 39 | 40 | 41 | 45 | 46 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search/flight-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject } from '@angular/core'; 2 | import { MatTableDataSource, MatTableModule } from '@angular/material/table'; 3 | import { DatePipe } from '@angular/common'; 4 | import { SelectionModel } from '@angular/cdk/collections'; 5 | import { Flight } from './flight'; 6 | import { FlightStore } from './flight-store'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { MatButtonModule } from '@angular/material/button'; 10 | 11 | @Component({ 12 | selector: 'demo-flight-search', 13 | templateUrl: 'flight-search.component.html', 14 | imports: [ 15 | MatTableModule, 16 | DatePipe, 17 | MatInputModule, 18 | FormsModule, 19 | MatButtonModule, 20 | ], 21 | }) 22 | export class FlightSearchComponent { 23 | searchParams: { from: string; to: string } = { from: 'Paris', to: 'London' }; 24 | flightStore = inject(FlightStore); 25 | 26 | displayedColumns: string[] = ['from', 'to', 'date']; 27 | dataSource = new MatTableDataSource([]); 28 | selection = new SelectionModel(true, []); 29 | 30 | constructor() { 31 | effect(() => { 32 | this.dataSource.data = this.flightStore.flights(); 33 | }); 34 | } 35 | 36 | search() { 37 | this.flightStore.loadFlights(this.searchParams); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search/flight-store.ts: -------------------------------------------------------------------------------- 1 | import { signalStore, withState } from '@ngrx/signals'; 2 | import { 3 | noPayload, 4 | payload, 5 | withDevtools, 6 | withRedux, 7 | updateState, 8 | } from '@angular-architects/ngrx-toolkit'; 9 | import { inject } from '@angular/core'; 10 | import { HttpClient, HttpParams } from '@angular/common/http'; 11 | import { map, switchMap } from 'rxjs'; 12 | import { Flight } from './flight'; 13 | 14 | const actions = { 15 | public: { 16 | loadFlights: payload<{ from: string; to: string }>(), 17 | delayFirst: noPayload, 18 | }, 19 | private: { 20 | flightsLoaded: payload<{ flights: Flight[] }>(), 21 | }, 22 | }; 23 | 24 | export const FlightStore = signalStore( 25 | { providedIn: 'root' }, 26 | withDevtools('flights'), 27 | withState({ flights: [] as Flight[] }), 28 | withRedux({ 29 | actions, 30 | reducer: (actions, on) => { 31 | on(actions.flightsLoaded, (state, { flights }) => { 32 | updateState(state, 'flights loaded', { flights }); 33 | }); 34 | }, 35 | 36 | effects: (actions, create) => { 37 | const httpClient = inject(HttpClient); 38 | 39 | return { 40 | loadFlights$: create(actions.loadFlights).pipe( 41 | switchMap(({ from, to }) => { 42 | return httpClient.get( 43 | 'https://demo.angulararchitects.io/api/flight', 44 | { 45 | params: new HttpParams().set('from', from).set('to', to), 46 | } 47 | ); 48 | }), 49 | map((flights) => actions.flightsLoaded({ flights })) 50 | ), 51 | }; 52 | }, 53 | }) 54 | ); 55 | -------------------------------------------------------------------------------- /apps/demo/src/app/flight-search/flight.ts: -------------------------------------------------------------------------------- 1 | export interface Flight { 2 | id: number; 3 | from: string; 4 | to: string; 5 | delayed: boolean; 6 | date: Date; 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo/src/app/immutable-state/immutable-state.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { patchState, signalStore, withMethods } from '@ngrx/signals'; 3 | import { withImmutableState } from '@angular-architects/ngrx-toolkit'; 4 | import { MatButton } from '@angular/material/button'; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | const initialState = { user: { id: 1, name: 'Konrad' } }; 8 | 9 | const UserStore = signalStore( 10 | { providedIn: 'root' }, 11 | withImmutableState(initialState), 12 | withMethods((store) => ({ 13 | mutateState() { 14 | patchState(store, (state) => { 15 | state.user.id = 2; 16 | return state; 17 | }); 18 | }, 19 | })) 20 | ); 21 | 22 | @Component({ 23 | template: ` 24 |

25 |
withImmutableState
26 |

27 |

28 | withImmutableState throws an error if the state is mutated, regardless 29 | inside or outside the SignalStore. 30 |

31 |
    32 |
  • 33 | 36 |
  • 37 |
  • 38 | 41 |
  • 42 |
43 | 44 |

Form to edit State mutable via ngModel

45 | 46 | `, 47 | imports: [MatButton, FormsModule], 48 | }) 49 | export class ImmutableStateComponent { 50 | protected readonly userStore = inject(UserStore); 51 | mutateOutside() { 52 | initialState.user.id = 2; 53 | } 54 | 55 | mutateInside() { 56 | this.userStore.mutateState(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/demo/src/app/lazy-routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | import { FlightSearchComponent } from './flight-search/flight-search.component'; 3 | import { FlightSearchSimpleComponent } from './flight-search-data-service-simple/flight-search-simple.component'; 4 | import { FlightEditSimpleComponent } from './flight-search-data-service-simple/flight-edit-simple.component'; 5 | import { FlightSearchDynamicComponent } from './flight-search-data-service-dynamic/flight-search.component'; 6 | import { FlightEditDynamicComponent } from './flight-search-data-service-dynamic/flight-edit.component'; 7 | import { TodoStorageSyncComponent } from './todo-storage-sync/todo-storage-sync.component'; 8 | import { FlightSearchWithPaginationComponent } from './flight-search-with-pagination/flight-search-with-pagination.component'; 9 | import { FlightSearchReducConnectorComponent } from './flight-search-redux-connector/flight-search.component'; 10 | import { provideFlightStore } from './flight-search-redux-connector/+state/redux'; 11 | import { TodoComponent } from './devtools/todo.component'; 12 | import { TodoIndexeddbSyncComponent } from './todo-indexeddb-sync/todo-indexeddb-sync.component'; 13 | 14 | export const lazyRoutes: Route[] = [ 15 | { path: 'todo', component: TodoComponent }, 16 | { path: 'flight-search', component: FlightSearchComponent }, 17 | { 18 | path: 'flight-search-data-service-simple', 19 | component: FlightSearchSimpleComponent, 20 | }, 21 | { path: 'flight-edit-simple/:id', component: FlightEditSimpleComponent }, 22 | { 23 | path: 'flight-search-data-service-dynamic', 24 | component: FlightSearchDynamicComponent, 25 | }, 26 | { 27 | path: 'flight-search-with-pagination', 28 | component: FlightSearchWithPaginationComponent, 29 | }, 30 | { path: 'flight-edit-dynamic/:id', component: FlightEditDynamicComponent }, 31 | { path: 'todo-storage-sync', component: TodoStorageSyncComponent }, 32 | { path: 'todo-indexeddb-sync', component: TodoIndexeddbSyncComponent }, 33 | { 34 | path: 'flight-search-redux-connector', 35 | providers: [provideFlightStore()], 36 | component: FlightSearchReducConnectorComponent, 37 | }, 38 | { 39 | path: 'reset', 40 | loadComponent: () => 41 | import('./reset/todo.component').then((m) => m.TodoComponent), 42 | }, 43 | { 44 | path: 'immutable-state', 45 | loadComponent: () => 46 | import('./immutable-state/immutable-state.component').then( 47 | (m) => m.ImmutableStateComponent 48 | ), 49 | }, 50 | { 51 | path: 'feature-factory', 52 | loadComponent: () => 53 | import('./feature-factory/feature-factory.component').then( 54 | (m) => m.FeatureFactoryComponent 55 | ), 56 | }, 57 | { 58 | path: 'conditional', 59 | loadComponent: () => 60 | import('./with-conditional/conditional.component').then( 61 | (m) => m.ConditionalSettingComponent 62 | ), 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /apps/demo/src/app/reset/todo-store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getState, 3 | patchState, 4 | signalStore, 5 | withHooks, 6 | withMethods, 7 | withState, 8 | } from '@ngrx/signals'; 9 | import { addEntity, updateEntity, withEntities } from '@ngrx/signals/entities'; 10 | import { setResetState, withReset } from '@angular-architects/ngrx-toolkit'; 11 | 12 | export interface Todo { 13 | id: number; 14 | name: string; 15 | finished: boolean; 16 | description?: string; 17 | deadline?: Date; 18 | } 19 | 20 | export type AddTodo = Omit; 21 | 22 | export const TodoStore = signalStore( 23 | { providedIn: 'root' }, 24 | withReset(), 25 | withEntities(), 26 | withState({ 27 | selectedIds: [] as number[], 28 | }), 29 | withMethods((store) => { 30 | let currentId = 0; 31 | return { 32 | _add(todo: AddTodo) { 33 | patchState(store, addEntity({ ...todo, id: ++currentId })); 34 | }, 35 | toggleFinished(id: number) { 36 | const todo = store.entityMap()[id]; 37 | patchState( 38 | store, 39 | updateEntity({ id, changes: { finished: !todo.finished } }) 40 | ); 41 | }, 42 | }; 43 | }), 44 | withHooks({ 45 | onInit: (store) => { 46 | store._add({ 47 | name: 'Go for a Walk', 48 | finished: false, 49 | description: 50 | 'Go for a walk in the park to relax and enjoy nature. Walking is a great way to clear your mind and get some exercise. It can help reduce stress and improve your mood. Make sure to wear comfortable shoes and bring a bottle of water. Enjoy the fresh air and take in the scenery around you.', 51 | }); 52 | 53 | store._add({ 54 | name: 'Read a Book', 55 | finished: false, 56 | description: 57 | 'Spend some time reading a book. It can be a novel, a non-fiction book, or any other genre you enjoy. Reading can help you relax and learn new things.', 58 | }); 59 | 60 | store._add({ 61 | name: 'Write a Journal', 62 | finished: false, 63 | description: 64 | 'Take some time to write in your journal. Reflect on your day, your thoughts, and your feelings. Journaling can be a great way to process emotions and document your life.', 65 | }); 66 | 67 | store._add({ 68 | name: 'Exercise', 69 | finished: false, 70 | description: 71 | 'Do some physical exercise. It can be a workout, a run, or any other form of exercise you enjoy. Exercise is important for maintaining physical and mental health.', 72 | }); 73 | 74 | store._add({ 75 | name: 'Cook a Meal', 76 | finished: false, 77 | description: 78 | 'Prepare a meal for yourself or your family. Cooking can be a fun and rewarding activity. Try out a new recipe or make one of your favorite dishes.', 79 | }); 80 | 81 | setResetState(store, getState(store)); 82 | }, 83 | }) 84 | ); 85 | -------------------------------------------------------------------------------- /apps/demo/src/app/reset/todo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject } from '@angular/core'; 2 | import { MatCheckboxModule } from '@angular/material/checkbox'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { MatTableDataSource, MatTableModule } from '@angular/material/table'; 5 | import { SelectionModel } from '@angular/cdk/collections'; 6 | import { Todo, TodoStore } from './todo-store'; 7 | import { MatButton } from '@angular/material/button'; 8 | 9 | @Component({ 10 | template: ` 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Name 33 | {{ element.name }} 34 | 35 | 36 | 37 | 41 | 42 |
43 | `, 44 | styles: `.button { 45 | margin-bottom: 1em; 46 | } 47 | `, 48 | imports: [MatCheckboxModule, MatIconModule, MatTableModule, MatButton], 49 | }) 50 | export class TodoComponent { 51 | todoStore = inject(TodoStore); 52 | 53 | displayedColumns: string[] = ['finished', 'name']; 54 | dataSource = new MatTableDataSource([]); 55 | selection = new SelectionModel(true, []); 56 | 57 | constructor() { 58 | effect(() => { 59 | this.dataSource.data = this.todoStore.entities(); 60 | }); 61 | } 62 | 63 | toggleFinished(todo: Todo) { 64 | this.todoStore.toggleFinished(todo.id); 65 | } 66 | 67 | resetState() { 68 | this.todoStore.resetState(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/demo/src/app/shared/flight-card.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

{{item.from}} - {{item.to}}

5 |
6 | 7 |
8 |

Flight-No.: #{{item.id}}

9 |

Date: {{item.date | date:'dd.MM.yyyy HH:mm:ss'}}

10 |

11 | 12 | 13 | 14 |

15 |
16 | 17 |
-------------------------------------------------------------------------------- /apps/demo/src/app/shared/flight-card.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from "@angular/common"; 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; 3 | import { RouterModule } from "@angular/router"; 4 | import { initFlight } from "./flight"; 5 | 6 | @Component({ 7 | selector: 'demo-flight-card', 8 | imports: [CommonModule, RouterModule], 9 | templateUrl: './flight-card.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class FlightCardComponent { 13 | @Input() item = initFlight; 14 | @Input() selected: boolean | undefined; 15 | @Output() selectedChange = new EventEmitter(); 16 | 17 | select() { 18 | this.selected = true; 19 | this.selectedChange.next(true); 20 | } 21 | 22 | deselect() { 23 | this.selected = false; 24 | this.selectedChange.next(false); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /apps/demo/src/app/shared/flight.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, firstValueFrom } from 'rxjs'; 4 | import { EntityId } from '@ngrx/signals/entities'; 5 | import { DataService } from '@angular-architects/ngrx-toolkit'; 6 | import { Flight } from './flight'; 7 | 8 | export type FlightFilter = { 9 | from: string; 10 | to: string; 11 | }; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class FlightService implements DataService { 17 | baseUrl = `https://demo.angulararchitects.io/api`; 18 | 19 | constructor(private http: HttpClient) {} 20 | 21 | loadById(id: EntityId): Promise { 22 | return firstValueFrom(this.findById('' + id)); 23 | } 24 | 25 | create(entity: Flight): Promise { 26 | entity.id = 0; 27 | return firstValueFrom(this.save(entity)); 28 | } 29 | 30 | update(entity: Flight): Promise { 31 | return firstValueFrom(this.save(entity)); 32 | } 33 | 34 | updateAll(): Promise { 35 | throw new Error('updateAll method not implemented.'); 36 | } 37 | 38 | delete(entity: Flight): Promise { 39 | return firstValueFrom(this.remove(entity)); 40 | } 41 | 42 | load(filter: FlightFilter): Promise { 43 | return firstValueFrom(this.find(filter.from, filter.to)); 44 | } 45 | 46 | private find(from: string, to: string, urgent = false): Observable { 47 | let url = [this.baseUrl, 'flight'].join('/'); 48 | 49 | if (urgent) { 50 | url = [this.baseUrl, 'error?code=403'].join('/'); 51 | } 52 | 53 | const params = new HttpParams().set('from', from).set('to', to); 54 | const headers = new HttpHeaders().set('Accept', 'application/json'); 55 | return this.http.get(url, { params, headers }); 56 | } 57 | 58 | private findById(id: string): Observable { 59 | const reqObj = { params: new HttpParams().set('id', id) }; 60 | const url = [this.baseUrl, 'flight'].join('/'); 61 | return this.http.get(url, reqObj); 62 | } 63 | 64 | private save(flight: Flight): Observable { 65 | const url = [this.baseUrl, 'flight'].join('/'); 66 | return this.http.post(url, flight); 67 | } 68 | 69 | private remove(flight: Flight): Observable { 70 | const url = [this.baseUrl, 'flight', flight.id].join('/'); 71 | return this.http.delete(url); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /apps/demo/src/app/shared/flight.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Flight = { 3 | id: number; 4 | from: string; 5 | to: string; 6 | date: string; 7 | delayed: boolean; 8 | } 9 | 10 | export const initFlight: Flight = { 11 | id: 0, 12 | from: '', 13 | to: '', 14 | date: '', 15 | delayed: false, 16 | }; 17 | -------------------------------------------------------------------------------- /apps/demo/src/app/shared/todo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export interface Todo { 4 | id: number; 5 | name: string; 6 | finished: boolean; 7 | description?: string; 8 | deadline?: Date; 9 | } 10 | 11 | export type AddTodo = Omit; 12 | 13 | @Injectable({ providedIn: 'root' }) 14 | export class TodoService { 15 | getData(): AddTodo[] { 16 | return [ 17 | { 18 | name: 'Go for a Walk', 19 | finished: false, 20 | description: 21 | 'Go for a walk in the park to relax and enjoy nature. Walking is a great way to clear your mind and get some exercise. It can help reduce stress and improve your mood. Make sure to wear comfortable shoes and bring a bottle of water. Enjoy the fresh air and take in the scenery around you.', 22 | }, 23 | { 24 | name: 'Read a Book', 25 | finished: false, 26 | description: 27 | 'Spend some time reading a book. It can be a novel, a non-fiction book, or any other genre you enjoy. Reading can help you relax and learn new things.', 28 | }, 29 | { 30 | name: 'Write a Journal', 31 | finished: false, 32 | description: 33 | 'Take some time to write in your journal. Reflect on your day, your thoughts, and your feelings. Journaling can be a great way to process emotions and document your life.', 34 | }, 35 | { 36 | name: 'Exercise', 37 | finished: false, 38 | description: 39 | 'Do some physical exercise. It can be a workout, a run, or any other form of exercise you enjoy. Exercise is important for maintaining physical and mental health.', 40 | }, 41 | { 42 | name: 'Cook a Meal', 43 | finished: false, 44 | description: 45 | 'Prepare a meal for yourself or your family. Cooking can be a fun and rewarding activity. Try out a new recipe or make one of your favorite dishes.', 46 | }, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/demo/src/app/todo-indexeddb-sync/synced-todo-store.ts: -------------------------------------------------------------------------------- 1 | import { getState, patchState, signalStore, withMethods } from '@ngrx/signals'; 2 | import { 3 | removeEntity, 4 | setEntity, 5 | updateEntity, 6 | withEntities, 7 | } from '@ngrx/signals/entities'; 8 | import { AddTodo, Todo, TodoService } from '../shared/todo.service'; 9 | import { 10 | withIndexeddb, 11 | withStorageSync, 12 | } from '@angular-architects/ngrx-toolkit'; 13 | import { inject } from '@angular/core'; 14 | 15 | export const SyncedTodoStore = signalStore( 16 | { providedIn: 'root' }, 17 | withEntities(), 18 | withStorageSync( 19 | { 20 | key: 'todos-indexeddb', 21 | }, 22 | withIndexeddb() 23 | ), 24 | withMethods((store, todoService = inject(TodoService)) => { 25 | let currentId = 0; 26 | return { 27 | add(todo: AddTodo) { 28 | store.readFromStorage(); 29 | patchState(store, setEntity({ id: ++currentId, ...todo })); 30 | }, 31 | 32 | remove(id: number) { 33 | patchState(store, removeEntity(id)); 34 | }, 35 | 36 | toggleFinished(id: number): void { 37 | const todo = store.entityMap()[id]; 38 | patchState( 39 | store, 40 | updateEntity({ id, changes: { finished: !todo.finished } }) 41 | ); 42 | }, 43 | 44 | reset() { 45 | const state = getState(store); 46 | 47 | state.ids.forEach((id) => this.remove(Number(id))); 48 | 49 | const todos = todoService.getData(); 50 | todos.forEach((todo) => this.add(todo)); 51 | }, 52 | }; 53 | }) 54 | ); 55 | -------------------------------------------------------------------------------- /apps/demo/src/app/todo-indexeddb-sync/todo-indexeddb-sync.component.html: -------------------------------------------------------------------------------- 1 |

StorageType:IndexedDB

2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | delete 15 | 16 | 17 | 18 | 19 | 20 | Name 21 | {{ element.name }} 22 | 23 | 24 | 25 | 26 | Description 27 | {{ element.description }} 28 | 29 | 30 | 31 | 32 | Deadline 34 | 35 | {{ element.deadline }} 37 | 38 | 39 | 40 | 41 | 45 | 46 | -------------------------------------------------------------------------------- /apps/demo/src/app/todo-indexeddb-sync/todo-indexeddb-sync.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/apps/demo/src/app/todo-indexeddb-sync/todo-indexeddb-sync.component.scss -------------------------------------------------------------------------------- /apps/demo/src/app/todo-indexeddb-sync/todo-indexeddb-sync.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject } from '@angular/core'; 2 | import { MatCheckboxModule } from '@angular/material/checkbox'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { MatTableDataSource, MatTableModule } from '@angular/material/table'; 5 | import { SyncedTodoStore } from './synced-todo-store'; 6 | import { SelectionModel } from '@angular/cdk/collections'; 7 | import { Todo } from '../shared/todo.service'; 8 | import { MatButton } from '@angular/material/button'; 9 | 10 | @Component({ 11 | selector: 'demo-todo-indexeddb-sync', 12 | imports: [MatCheckboxModule, MatIconModule, MatTableModule, MatButton], 13 | templateUrl: './todo-indexeddb-sync.component.html', 14 | styleUrl: './todo-indexeddb-sync.component.scss', 15 | standalone: true, 16 | }) 17 | export class TodoIndexeddbSyncComponent { 18 | todoStore = inject(SyncedTodoStore); 19 | 20 | displayedColumns: string[] = ['finished', 'name', 'description', 'deadline']; 21 | dataSource = new MatTableDataSource([]); 22 | selection = new SelectionModel(true, []); 23 | 24 | constructor() { 25 | effect(() => { 26 | this.dataSource.data = this.todoStore.entities(); 27 | }); 28 | } 29 | 30 | checkboxLabel(todo: Todo) { 31 | this.todoStore.toggleFinished(todo.id); 32 | } 33 | 34 | removeTodo(todo: Todo) { 35 | this.todoStore.remove(todo.id); 36 | } 37 | 38 | onClickReset() { 39 | this.todoStore.reset(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/demo/src/app/todo-storage-sync/synced-todo-store.ts: -------------------------------------------------------------------------------- 1 | import { getState, patchState, signalStore, withMethods } from '@ngrx/signals'; 2 | import { 3 | removeEntity, 4 | setEntity, 5 | updateEntity, 6 | withEntities, 7 | } from '@ngrx/signals/entities'; 8 | import { 9 | withLocalStorage, 10 | withStorageSync, 11 | } from '@angular-architects/ngrx-toolkit'; 12 | import { AddTodo, Todo, TodoService } from '../shared/todo.service'; 13 | import { inject } from '@angular/core'; 14 | 15 | export const SyncedTodoStore = signalStore( 16 | { providedIn: 'root' }, 17 | withEntities(), 18 | withStorageSync('todos', withLocalStorage()), 19 | withMethods((store, todoService = inject(TodoService)) => { 20 | let currentId = 0; 21 | return { 22 | add(todo: AddTodo) { 23 | patchState(store, setEntity({ id: ++currentId, ...todo })); 24 | }, 25 | 26 | remove(id: number) { 27 | patchState(store, removeEntity(id)); 28 | }, 29 | 30 | toggleFinished(id: number): void { 31 | const todo = store.entityMap()[id]; 32 | patchState( 33 | store, 34 | updateEntity({ id, changes: { finished: !todo.finished } }) 35 | ); 36 | }, 37 | 38 | reset() { 39 | const state = getState(store); 40 | 41 | state.ids.forEach((id) => this.remove(Number(id))); 42 | 43 | const todos = todoService.getData(); 44 | todos.forEach((todo) => this.add(todo)); 45 | }, 46 | }; 47 | }) 48 | ); 49 | -------------------------------------------------------------------------------- /apps/demo/src/app/todo-storage-sync/todo-storage-sync.component.html: -------------------------------------------------------------------------------- 1 |

StorageType:LocalStorage

2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | delete 15 | 16 | 17 | 18 | 19 | 20 | Name 21 | {{ element.name }} 22 | 23 | 24 | 25 | 26 | Description 27 | {{ element.description }} 28 | 29 | 30 | 31 | 32 | Deadline 34 | 35 | {{ element.deadline }} 37 | 38 | 39 | 40 | 41 | 45 | 46 | -------------------------------------------------------------------------------- /apps/demo/src/app/todo-storage-sync/todo-storage-sync.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/apps/demo/src/app/todo-storage-sync/todo-storage-sync.component.scss -------------------------------------------------------------------------------- /apps/demo/src/app/todo-storage-sync/todo-storage-sync.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, inject } from '@angular/core'; 2 | import { MatCheckboxModule } from '@angular/material/checkbox'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { MatTableDataSource, MatTableModule } from '@angular/material/table'; 5 | import { SyncedTodoStore } from './synced-todo-store'; 6 | import { SelectionModel } from '@angular/cdk/collections'; 7 | import { Todo } from '../shared/todo.service'; 8 | import { MatButton } from '@angular/material/button'; 9 | 10 | @Component({ 11 | selector: 'demo-todo-storage-sync', 12 | imports: [MatCheckboxModule, MatIconModule, MatTableModule, MatButton], 13 | templateUrl: './todo-storage-sync.component.html', 14 | styleUrl: './todo-storage-sync.component.scss', 15 | }) 16 | export class TodoStorageSyncComponent { 17 | todoStore = inject(SyncedTodoStore); 18 | 19 | displayedColumns: string[] = ['finished', 'name', 'description', 'deadline']; 20 | dataSource = new MatTableDataSource([]); 21 | selection = new SelectionModel(true, []); 22 | 23 | constructor() { 24 | effect(() => { 25 | this.dataSource.data = this.todoStore.entities(); 26 | }); 27 | } 28 | 29 | checkboxLabel(todo: Todo) { 30 | this.todoStore.toggleFinished(todo.id); 31 | } 32 | 33 | removeTodo(todo: Todo) { 34 | this.todoStore.remove(todo.id); 35 | } 36 | 37 | onClickReset() { 38 | this.todoStore.reset(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/demo/src/app/with-conditional/conditional.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, signal, inject, untracked, effect } from '@angular/core'; 2 | import { 3 | patchState, 4 | signalStore, 5 | signalStoreFeature, 6 | withHooks, 7 | withMethods, 8 | withState, 9 | } from '@ngrx/signals'; 10 | import { FormsModule } from '@angular/forms'; 11 | import { 12 | MatButtonToggle, 13 | MatButtonToggleGroup, 14 | } from '@angular/material/button-toggle'; 15 | import { withConditional } from '@angular-architects/ngrx-toolkit'; 16 | import { MatButton } from '@angular/material/button'; 17 | 18 | const withUser = signalStoreFeature( 19 | withState({ id: 0, name: '' }), 20 | withHooks((store) => ({ 21 | onInit() { 22 | patchState(store, { id: 1, name: 'Konrad' }); 23 | }, 24 | })) 25 | ); 26 | 27 | const withFakeUser = signalStoreFeature( 28 | withState({ id: 0, name: 'Tommy Fake' }) 29 | ); 30 | 31 | const UserServiceStore = signalStore( 32 | { providedIn: 'root' }, 33 | withState({ implementation: 'real' as 'real' | 'fake' }), 34 | withMethods((store) => ({ 35 | setImplementation(implementation: 'real' | 'fake') { 36 | patchState(store, { implementation }); 37 | }, 38 | })) 39 | ); 40 | 41 | const UserStore = signalStore( 42 | withConditional( 43 | () => inject(UserServiceStore).implementation() === 'real', 44 | withUser, 45 | withFakeUser 46 | ) 47 | ); 48 | 49 | @Component({ 50 | selector: 'demo-conditional-user', 51 | template: `

Current User {{ userStore.name() }}

`, 52 | providers: [UserStore], 53 | }) 54 | class ConditionalUserComponent { 55 | protected readonly userStore = inject(UserStore); 56 | 57 | constructor() { 58 | console.log('log geht es'); 59 | } 60 | } 61 | 62 | @Component({ 63 | template: ` 64 |

65 |
withConditional
66 |

67 | 68 | 72 | Real User 73 | Fake User 74 | 75 | 76 |
77 | 80 |
81 | @if (showUserComponent()) { 82 | 83 | } 84 | `, 85 | imports: [ 86 | FormsModule, 87 | MatButtonToggle, 88 | MatButtonToggleGroup, 89 | ConditionalUserComponent, 90 | MatButton, 91 | ], 92 | }) 93 | export class ConditionalSettingComponent { 94 | showUserComponent = signal(false); 95 | 96 | toggleUserComponent() { 97 | this.showUserComponent.update((show) => !show); 98 | } 99 | userService = inject(UserServiceStore); 100 | protected readonly userFeature = signal<'real' | 'fake'>('real'); 101 | 102 | effRef = effect(() => { 103 | const userFeature = this.userFeature(); 104 | 105 | untracked(() => { 106 | this.userService.setImplementation(userFeature); 107 | this.showUserComponent.set(false); 108 | }); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /apps/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/apps/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/apps/demo/src/favicon.ico -------------------------------------------------------------------------------- /apps/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apps/demo/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).catch((err) => 6 | console.error(err) 7 | ); 8 | -------------------------------------------------------------------------------- /apps/demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | 6 | .app-container { 7 | margin-top: 60px; 8 | margin-left: 40px; 9 | margin-right: 40px; 10 | } 11 | 12 | .active-card { 13 | background-color: antiquewhite; 14 | } -------------------------------------------------------------------------------- /apps/demo/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 2 | globalThis.ngJest = { 3 | testEnvironmentOptions: { 4 | errorOnUnknownElements: true, 5 | errorOnUnknownProperties: true, 6 | }, 7 | }; 8 | import 'jest-preset-angular/setup-jest'; 9 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": ["src/main.ts"], 8 | "include": ["src/**/*.d.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true 12 | }, 13 | "files": [], 14 | "include": [], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.app.json" 18 | }, 19 | { 20 | "path": "./tsconfig.spec.json" 21 | }, 22 | { 23 | "path": "./tsconfig.editor.json" 24 | } 25 | ], 26 | "extends": "../../tsconfig.base.json", 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/devtools.png -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | /docs/api/ 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ```shell 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ```shell 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ```shell 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ```shell 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ```shell 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/docs/extensions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extensions 3 | --- 4 | 5 | The NgRx Toolkit is a set of extensions to the NgRx SignalsStore. 6 | 7 | It offers extensions like: 8 | 9 | - [⭐️ Devtools](./with-devtools): Integration into Redux Devtools 10 | - [Conditional Features](./with-conditional): Allows adding features to the store conditionally 11 | - [DataService](./with-data-service): Builds on top of `withEntities` and adds the backend synchronization to it 12 | - [Immutable State Protection](./with-immutable-state): Protects the state from being mutated outside or inside the Store. 13 | - [~Redux~](./with-redux): Possibility to use the Redux Pattern. Deprecated in favor of NgRx's `@ngrx/signals/events` starting in 19.2 14 | - [Reset](./with-reset): Adds a `resetState` method to your store 15 | - [Call State](./with-call-state): Add call state management to your signal stores 16 | - [Storage Sync](./with-storage-sync): Synchronizes the Store with Web Storage 17 | - [Undo Redo](./with-undo-redo): Adds Undo/Redo functionality to your store 18 | 19 | To install it, run 20 | 21 | ```shell 22 | npm i @angular-architects/ngrx-toolkit 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/docs/with-conditional.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: withConditional() 3 | --- 4 | 5 | ```typescript 6 | import { withConditional } from '@angular-architects/ngrx-toolkit'; 7 | ``` 8 | 9 | `withConditional` activates a feature based on a given condition. 10 | 11 | ## Use Cases 12 | 13 | - Conditionally activate features based on the **store state** or other criteria. 14 | - Choose between **two different implementations** of a feature. 15 | 16 | ## Type Constraints 17 | 18 | Both features must have **exactly the same state, props, and methods**. 19 | Otherwise, a type error will occur. 20 | 21 | ## Usage 22 | 23 | ```typescript 24 | import { withConditional } from '@angular-architects/ngrx-toolkit'; 25 | 26 | const withUser = signalStoreFeature( 27 | withState({ id: 1, name: 'Konrad' }), 28 | withHooks((store) => ({ 29 | onInit() { 30 | // user loading logic 31 | }, 32 | })) 33 | ); 34 | 35 | function withFakeUser() { 36 | return signalStoreFeature(withState({ id: 0, name: 'anonymous' })); 37 | } 38 | 39 | signalStore( 40 | withMethods(() => ({ 41 | useRealUser: () => true, 42 | })), 43 | withConditional((store) => store.useRealUser(), withUser, withFakeUser) 44 | ); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/docs/with-feature-factory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: withFeatureFactory() 3 | --- 4 | 5 | ```typescript 6 | // DEPRECATED 7 | import { withFeatureFactory } from '@angular-architects/ngrx-toolkit'; 8 | 9 | // Use this instead 10 | import { withFeature } from '@ngrx/signals' 11 | ``` 12 | 13 | The `withFeatureFactory()` function allows passing properties, methods, or signals from a SignalStore to a feature. It is an advanced feature, primarily targeted for library authors for SignalStore features. 14 | 15 | :::warning 16 | ## Deprecation 17 | 18 | Use `import { withFeature } from '@ngrx/signals'` instead. 19 | 20 | [`withFeature`](https://ngrx.io/guide/signals/signal-store/custom-store-features#connecting-a-custom-feature-with-the-store) is the successor of the toolkit's `withFeatureFactory`. 21 | - Available starting in `ngrx/signals` 19.1 22 | - NgRx PR: ["feat(signals): add `withFeature` #4739"](https://github.com/ngrx/platform/pull/4739) 23 | - NgRx [documentation section](https://ngrx.io/guide/signals/signal-store/custom-store-features#connecting-a-custom-feature-with-the-store) on `withFeature` 24 | 25 | In the future, `withFeatureFactory` will likely be removed, provided a right migration path is prepared. Watch out for PRs, and see [the PR that deprecates `withFeatureFactory` for the initial plan for handling the removal](https://github.com/angular-architects/ngrx-toolkit/pull/167#pullrequestreview-2735443379). 26 | ::: 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docs/with-immutable-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: withImmutableState() 3 | --- 4 | 5 | ```typescript 6 | import { withImmutableState } from '@angular-architects/ngrx-toolkit'; 7 | ``` 8 | 9 | `withImmutableState` acts like `withState` but protects 10 | the state against unintended mutable changes, by throwing 11 | a runtime error. 12 | 13 | The protection is not limited to changes within the 14 | SignalStore but also outside of it. 15 | 16 | ```typescript 17 | import { withImmutableState } from '@angular-architects/ngrx-toolkit'; 18 | 19 | const initialState = { user: { id: 1, name: 'Konrad' } }; 20 | 21 | const UserStore = signalStore( 22 | { providedIn: 'root' }, 23 | withImmutableState(initialState), 24 | withMethods((store) => ({ 25 | mutateState() { 26 | patchState(store, (state) => { 27 | state.user.id = 2; 28 | return state; 29 | }); 30 | }, 31 | })) 32 | ); 33 | ``` 34 | 35 | If `mutateState` is called, a runtime error will be thrown. 36 | 37 | ```typescript 38 | class SomeComponent { 39 | userStore = inject(UserStore); 40 | 41 | mutateChange() { 42 | this.userStore.mutateState(); // 🔥 throws an error 43 | } 44 | } 45 | ``` 46 | 47 | The same is also true, when `initialState` is changed: 48 | 49 | ```typescript 50 | initialState.user.id = 2; // 🔥 throws an error 51 | ``` 52 | 53 | Finally, it could also happen, if third-party libraries or the Angular API does mutations to the state. 54 | 55 | A common example is the usage in template-driven forms: 56 | 57 | ```typescript 58 | @Component({ 59 | template: ` `, 60 | }) 61 | class SomeComponent {} 62 | ``` 63 | 64 | ## Protection in production mode 65 | 66 | By default, `withImmutableState` is only active in development mode. 67 | 68 | There is a way to enable it in production mode as well: 69 | 70 | ```typescript 71 | import { withImmutableState } from '@angular-architects/ngrx-toolkit'; 72 | 73 | const UserStore = signalStore({ providedIn: 'root' }, withImmutableState(initialState, { enableInProduction: true })); 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/docs/with-reset.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: withReset() 3 | --- 4 | 5 | ```typescript 6 | import { withReset } from '@angular-architects/ngrx-toolkit'; 7 | ``` 8 | 9 | `withReset()` adds a method to reset the state of the Signal Store to its initial value. Nothing more to say about it 😅 10 | 11 | Example: 12 | 13 | ```typescript 14 | import { withReset } from '@angular-architects/ngrx-toolkit'; 15 | 16 | const Store = signalStore( 17 | withState({ 18 | user: { id: 1, name: 'Konrad' }, 19 | address: { city: 'Vienna', zip: '1010' }, 20 | }), 21 | withReset(), // <-- the reset extension 22 | withMethods((store) => ({ 23 | changeUser(id: number, name: string) { 24 | patchState(store, { user: { id, name } }); 25 | }, 26 | changeUserName(name: string) { 27 | patchState(store, (value) => ({ user: { ...value.user, name } })); 28 | }, 29 | })) 30 | ); 31 | 32 | const store = new Store(); 33 | 34 | store.changeUser(2, 'John'); 35 | console.log(store.user()); // { id: 2, name: 'John' } 36 | 37 | store.resetState(); 38 | console.log(store.user()); // { id: 1, name: 'Konrad' } 39 | ``` 40 | 41 | ## `setResetState()` 42 | 43 | ```typescript 44 | import { setResetState } from '@angular-architects/ngrx-toolkit'; 45 | ``` 46 | 47 | If you want to set a custom reset state, you can use the `setResetState()` method. 48 | 49 | Example: 50 | 51 | ```typescript 52 | // continue from the previous example 53 | import { setResetState } from '@angular-architects/ngrx-toolkit'; 54 | 55 | setResetState(store, { user: { id: 3, name: 'Jane' }, address: { city: 'Berlin', zip: '10115' } }); 56 | store.changeUser(4, 'Alice'); 57 | 58 | store.resetState(); 59 | console.log(store.user()); // { id: 3, name: 'Jane' } 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/docs/with-storage-sync.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: withStorageSync() 3 | --- 4 | 5 | ```typescript 6 | import { withStorageSync } from '@angular-architects/ngrx-toolkit'; 7 | ``` 8 | 9 | `withStorageSync` adds automatic or manual synchronization with Web Storage (`localstorage`/`sessionstorage`). 10 | 11 | :::warning 12 | As Web Storage only works in browser environments it will fallback to a stub implementation on server environments. 13 | ::: 14 | 15 | Example: 16 | 17 | ```typescript 18 | import { withStorageSync } from '@angular-architects/ngrx-toolkit'; 19 | 20 | const SyncStore = signalStore( 21 | withStorageSync({ 22 | key: 'synced', // key used when writing to/reading from storage 23 | autoSync: false, // read from storage on init and write on state changes - `true` by default 24 | select: (state: User) => Partial, // projection to keep specific slices in sync 25 | parse: (stateString: string) => State, // custom parsing from storage - `JSON.parse` by default 26 | stringify: (state: User) => string, // custom stringification - `JSON.stringify` by default 27 | storage: () => sessionstorage, // factory to select storage to sync with 28 | }) 29 | ); 30 | ``` 31 | 32 | ```typescript 33 | @Component(...) 34 | public class SyncedStoreComponent { 35 | private syncStore = inject(SyncStore); 36 | 37 | updateFromStorage(): void { 38 | this.syncStore.readFromStorage(); // reads the stored item from storage and patches the state 39 | } 40 | 41 | updateStorage(): void { 42 | this.syncStore.writeToStorage(); // writes the current state to storage 43 | } 44 | 45 | clearStorage(): void { 46 | this.syncStore.clearStorage(); // clears the stored item in storage 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/docs/with-undo-redo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: withUndoRedo() 3 | --- 4 | 5 | ```typescript 6 | import { withUndoRedo } from '@angular-architects/ngrx-toolkit'; 7 | ``` 8 | 9 | `withUndoRedo` adds undo and redo functionality to the store. 10 | 11 | Example: 12 | 13 | ```typescript 14 | import { withUndoRedo } from '@angular-architects/ngrx-toolkit'; 15 | 16 | const SyncStore = signalStore( 17 | withUndoRedo({ 18 | maxStackSize: 100, // limit of undo/redo steps - `100` by default 19 | collections: ['flight'], // entity collections to keep track of - unnamed collection is tracked by default 20 | keys: ['test'], // non-entity based keys to track - `[]` by default 21 | skip: 0, // number of initial state changes to skip - `0` by default 22 | }) 23 | ); 24 | ``` 25 | 26 | ```typescript 27 | @Component(...) 28 | public class UndoRedoComponent { 29 | private syncStore = inject(SyncStore); 30 | 31 | canUndo = this.store.canUndo; // use in template or in ts 32 | canRedo = this.store.canRedo; // use in template or in ts 33 | clearStack = this.store.clearStack; // use in template or in ts 34 | 35 | undo(): void { 36 | if (!this.canUndo()) return; 37 | this.store.undo(); 38 | } 39 | 40 | redo(): void { 41 | if (!this.canRedo()) return; 42 | this.store.redo(); 43 | } 44 | 45 | clearStack(): void { 46 | this.store.clearStack(); 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.7.0", 19 | "@docusaurus/preset-classic": "3.7.0", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.0.0", 22 | "docusaurus-plugin-typedoc": "^1.2.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0", 26 | "typedoc": "^0.27.6", 27 | "typedoc-plugin-markdown": "^4.4.1" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "3.7.0", 31 | "@docusaurus/tsconfig": "3.7.0", 32 | "@docusaurus/types": "3.7.0", 33 | "typescript": "~5.6.2" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 3 chrome version", 43 | "last 3 firefox version", 44 | "last 5 safari version" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=18.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | extensionSidebar: [ 18 | 'extensions', 19 | 'with-data-service', 20 | 'with-devtools', 21 | 'with-redux', 22 | 'with-storage-sync', 23 | 'with-undo-redo', 24 | 'with-reset', 25 | 'with-immutable-state', 26 | 'with-feature-factory', 27 | 'with-conditional', 28 | 'with-call-state', 29 | ], 30 | reduxConnectorSidebar: [ 31 | { 32 | type: 'category', 33 | label: 'Redux Connector', 34 | items: ['create-redux-state'], 35 | }, 36 | ], 37 | 38 | // But you can create a sidebar manually 39 | /* 40 | tutorialSidebar: [ 41 | 'intro', 42 | 'hello', 43 | { 44 | type: 'category', 45 | label: 'Tutorial', 46 | items: ['tutorial-basics/create-a-document'], 47 | }, 48 | ], 49 | */ 50 | }; 51 | 52 | export default sidebars; 53 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: ReactNode; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({ title, Svg, description }: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 | {title} 52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): ReactNode { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | 13 | .container { 14 | height: 100% 15 | } 16 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | :root { 8 | --ifm-color-primary: #6699CC; 9 | } 10 | 11 | /* You can override the default Infima variables here. */ 12 | :root { 13 | --ifm-color-primary-dark: #29784c; 14 | --ifm-color-primary-darker: #277148; 15 | --ifm-color-primary-darkest: #205d3b; 16 | --ifm-color-primary-light: #33925d; 17 | --ifm-color-primary-lighter: #359962; 18 | --ifm-color-primary-lightest: #3cad6e; 19 | --ifm-code-font-size: 95%; 20 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 21 | } 22 | 23 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 24 | [data-theme='dark'] { 25 | --ifm-color-primary-dark: #21af90; 26 | --ifm-color-primary-darker: #1fa588; 27 | --ifm-color-primary-darkest: #1a8870; 28 | --ifm-color-primary-light: #29d5b0; 29 | --ifm-color-primary-lighter: #32d8b4; 30 | --ifm-color-primary-lightest: #4fddbf; 31 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 32 | } 33 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | flex: 1; 8 | padding: 4rem 0; 9 | text-align: center; 10 | position: relative; 11 | overflow: hidden; 12 | } 13 | 14 | @media screen and (max-width: 996px) { 15 | .heroBanner { 16 | padding: 2rem; 17 | } 18 | } 19 | 20 | .buttons { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | .logo { 27 | max-width: 250px; 28 | } 29 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import Heading from '@theme/Heading'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | 16 | {siteConfig.title} 17 | 18 | 19 | logo 20 | 21 |

{siteConfig.tagline}

22 |
23 | 27 | Show Me! 28 | 29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default function Home(): ReactNode { 36 | const { siteConfig } = useDocusaurusContext(); 37 | return ( 38 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/static/img/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/static/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/static/img/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/devtools.png -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/favicon-16x16.png -------------------------------------------------------------------------------- /docs/static/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/favicon-32x32.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/favicon_io.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/favicon_io.zip -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/static/img/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /docs/static/img/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/docs/static/img/social.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /docs/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["../libs/ngrx-toolkit/src/index.ts"], 3 | "out": "docs/api", 4 | "exclude": ["**/*.test.ts", "**/__tests__/**/*"], // Exclude test files 5 | "includeVersion": true, // Optionally include version info 6 | "excludeExternals": true // Exclude external libraries 7 | } 8 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const nx = require('@nx/eslint-plugin'); 2 | const unusedImports = require('eslint-plugin-unused-imports'); 3 | 4 | module.exports = [ 5 | ...nx.configs['flat/base'], 6 | ...nx.configs['flat/typescript'], 7 | ...nx.configs['flat/javascript'], 8 | { 9 | ignores: ['**/dist'], 10 | }, 11 | { 12 | files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], 13 | rules: { 14 | '@nx/enforce-module-boundaries': [ 15 | 'error', 16 | { 17 | enforceBuildableLibDependency: true, 18 | allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'], 19 | depConstraints: [ 20 | { 21 | sourceTag: '*', 22 | onlyDependOnLibsWithTags: ['*'], 23 | }, 24 | ], 25 | }, 26 | ], 27 | }, 28 | }, 29 | { 30 | files: [ 31 | '**/*.ts', 32 | '**/*.tsx', 33 | '**/*.js', 34 | '**/*.jsx', 35 | '**/*.cjs', 36 | '**/*.mjs', 37 | ], 38 | // Override or add rules here 39 | rules: {}, 40 | }, 41 | { 42 | files: ['**/*.json'], 43 | rules: { 44 | '@nx/dependency-checks': [ 45 | 'error', 46 | { 47 | ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'], 48 | }, 49 | ], 50 | }, 51 | languageOptions: { 52 | parser: require('jsonc-eslint-parser'), 53 | }, 54 | }, 55 | { 56 | plugins: { "unused-imports": unusedImports }, 57 | rules: { 58 | "@typescript-eslint/no-unused-vars": "off", 59 | "unused-imports/no-unused-imports": "error", 60 | "unused-imports/no-unused-vars": [ 61 | "warn", 62 | { 63 | vars: "all", 64 | varsIgnorePattern: "^_", 65 | args: "after-used", 66 | argsIgnorePattern: "^_", 67 | }, 68 | ], 69 | }, 70 | } 71 | ]; 72 | -------------------------------------------------------------------------------- /integration-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This uses different NgRx versions and verifies that the demo app builds. 4 | 5 | set -e 6 | 7 | echo 'checking against different @ngrx/signals versions' 8 | 9 | ./read-supported-versions.js 10 | 11 | i=0 12 | while read line 13 | do 14 | versions[$i]="$line" 15 | i=$((i+1)) 16 | done < versions.txt 17 | 18 | for version in ${versions[*]}; do 19 | pnpm i @ngrx/signals@$version 20 | echo "Building with version $version" 21 | pnpm nx build --project demo 22 | done 23 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjectsAsync } from '@nx/jest'; 2 | 3 | export default async () => ({ 4 | projects: await getJestProjectsAsync(), 5 | }); 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/README.md: -------------------------------------------------------------------------------- 1 | # NgRx Toolkit 2 | 3 | 4 | 5 | NgRx Toolkit is a set of extensions to the NgRx Signals Store, like 6 | 7 | - Devtools: Integration into Redux Devtools 8 | - Redux: Possibility to use the Redux Pattern (Reducer, Actions, Effects) 9 | - Storage Sync: Synchronize the Store with Web Storage 10 | - Redux Connector: Map NgRx Store Actions to a present Signal Store 11 | 12 | For a more detailed guide on installation, setup, and usage, head to the [**Documentation**](https://ngrx-toolkit.angulararchitects.io//). 13 | 14 | ## https://ngrx-toolkit.angulararchitects.io/ 15 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const nx = require('@nx/eslint-plugin'); 2 | const baseConfig = require('../../eslint.config.cjs'); 3 | 4 | module.exports = [ 5 | ...baseConfig, 6 | { 7 | files: ['**/*.json'], 8 | rules: { 9 | '@nx/dependency-checks': [ 10 | 'error', 11 | { 12 | ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'], 13 | }, 14 | ], 15 | }, 16 | languageOptions: { 17 | parser: require('jsonc-eslint-parser'), 18 | }, 19 | }, 20 | ...nx.configs['flat/angular'], 21 | ...nx.configs['flat/angular-template'], 22 | { 23 | files: ['**/*.ts'], 24 | rules: { 25 | '@angular-eslint/directive-selector': [ 26 | 'error', 27 | { 28 | type: 'attribute', 29 | prefix: 'lib', 30 | style: 'camelCase', 31 | }, 32 | ], 33 | '@angular-eslint/component-selector': [ 34 | 'error', 35 | { 36 | type: 'element', 37 | prefix: 'lib', 38 | style: 'kebab-case', 39 | }, 40 | ], 41 | }, 42 | } 43 | ]; 44 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | displayName: 'ngrx-toolkit', 3 | setupFiles: ['fake-indexeddb/auto', 'core-js'], 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: '../../coverage/libs/ngrx-toolkit', 7 | transform: { 8 | '^.+\\.(ts|mjs|js|html)$': [ 9 | 'jest-preset-angular', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$', 13 | }, 14 | ], 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/ngrx-toolkit", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-architects/ngrx-toolkit", 3 | "version": "19.1.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "GitHub", 7 | "url": "https://github.com/angular-architects/ngrx-toolkit" 8 | }, 9 | "peerDependencies": { 10 | "@angular/core": "^19.0.0", 11 | "@angular/common": "^19.0.0", 12 | "@ngrx/signals": "^19.1.0", 13 | "@ngrx/store": "^19.1.0", 14 | "rxjs": "^7.0.0" 15 | }, 16 | "peerDependenciesMeta": { 17 | "@ngrx/store": { 18 | "optional": true 19 | } 20 | }, 21 | "dependencies": {}, 22 | "sideEffects": false 23 | } 24 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-toolkit", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/ngrx-toolkit/src", 5 | "prefix": "ngrx-toolkit", 6 | "projectType": "library", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@nx/angular:package", 11 | "outputs": ["{workspaceRoot}/dist/{projectRoot}"], 12 | "options": { 13 | "project": "libs/ngrx-toolkit/ng-package.json" 14 | }, 15 | "configurations": { 16 | "production": { 17 | "tsConfig": "libs/ngrx-toolkit/tsconfig.lib.prod.json" 18 | }, 19 | "development": { 20 | "tsConfig": "libs/ngrx-toolkit/tsconfig.lib.json" 21 | } 22 | }, 23 | "defaultConfiguration": "production" 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 28 | "options": { 29 | "jestConfig": "libs/ngrx-toolkit/jest.config.ts" 30 | } 31 | }, 32 | "lint": { 33 | "executor": "@nx/eslint:lint", 34 | "outputs": ["{options.outputFile}"] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createReduxState, 3 | mapAction, 4 | withActionMappers, 5 | } from './src/lib/create-redux'; 6 | export { reduxMethod } from './src/lib/rxjs-interop/redux-method'; 7 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib": { 3 | "entryFile": "index.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/src/lib/create-redux.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | makeEnvironmentProviders, 4 | provideEnvironmentInitializer, 5 | } from '@angular/core'; 6 | import { ActionCreator, ActionType } from '@ngrx/store/src/models'; 7 | import { 8 | CreateReduxState, 9 | ExtractActionTypes, 10 | MapperTypes, 11 | ServiceWithDecorator, 12 | Store, 13 | } from './model'; 14 | import { SignalReduxStore, injectReduxDispatch } from './signal-redux-store'; 15 | import { capitalize, isActionCreator } from './util'; 16 | 17 | export function mapAction( 18 | ...args: [ 19 | ...creators: Creators, 20 | storeMethod: (action: ActionType) => unknown 21 | ] 22 | ): MapperTypes; 23 | export function mapAction( 24 | ...args: [ 25 | ...creators: Creators, 26 | storeMethod: ( 27 | action: ActionType, 28 | resultMethod: (input: T) => unknown 29 | ) => unknown, 30 | resultMethod: (input: T) => unknown 31 | ] 32 | ): MapperTypes; 33 | export function mapAction( 34 | ...args: [ 35 | ...creators: Creators, 36 | storeMethod: (action: ActionType) => unknown, 37 | resultMethod?: (input: unknown) => unknown 38 | ] 39 | ): MapperTypes { 40 | let resultMethod = args.pop() as unknown as 41 | | ((input: unknown) => unknown) 42 | | undefined; 43 | let storeMethod = args.pop() as unknown as ( 44 | action: ActionType 45 | ) => unknown; 46 | 47 | if (isActionCreator(storeMethod)) { 48 | args.push(storeMethod); 49 | storeMethod = resultMethod || storeMethod; 50 | resultMethod = undefined; 51 | } 52 | 53 | const types = (args as unknown as Creators).map( 54 | (creator) => creator.type 55 | ) as unknown as ExtractActionTypes; 56 | 57 | return { 58 | types, 59 | storeMethod, 60 | resultMethod, 61 | }; 62 | } 63 | 64 | export function withActionMappers( 65 | ...mappers: MapperTypes[] 66 | ): MapperTypes[] { 67 | return mappers; 68 | } 69 | 70 | export function createReduxState( 71 | storeName: StoreName, 72 | signalStore: STORE, 73 | withActionMappers: ( 74 | store: InstanceType 75 | ) => MapperTypes[] 76 | ): CreateReduxState { 77 | const isRootProvider = 78 | (signalStore as ServiceWithDecorator)?.ɵprov?.providedIn === 'root'; 79 | return { 80 | [`provide${capitalize(storeName)}Store`]: (connectReduxDevtools = false) => 81 | makeEnvironmentProviders([ 82 | isRootProvider ? [] : signalStore, 83 | provideEnvironmentInitializer(() => { 84 | const initializerFn = ( 85 | ( 86 | signalReduxStore = inject(SignalReduxStore), 87 | store = inject(signalStore) 88 | ) => 89 | () => { 90 | if (connectReduxDevtools) { 91 | // addStoreToReduxDevtools(store, storeName, false); 92 | } 93 | signalReduxStore.connectFeatureStore(withActionMappers(store)); 94 | } 95 | )(); 96 | return initializerFn(); 97 | }), 98 | ]), 99 | [`inject${capitalize(storeName)}Store`]: () => 100 | Object.assign(inject(signalStore), { dispatch: injectReduxDispatch() }), 101 | } as CreateReduxState; 102 | } 103 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/src/lib/model.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentProviders, Signal, Type } from '@angular/core'; 2 | import { DeepSignal } from '@ngrx/signals/src/deep-signal'; 3 | import { 4 | SignalStoreFeatureResult, 5 | StateSignals, 6 | } from '@ngrx/signals/src/signal-store-models'; 7 | import { 8 | Action, 9 | ActionCreator, 10 | ActionType, 11 | Prettify, 12 | } from '@ngrx/store/src/models'; 13 | import { Observable, Unsubscribable } from 'rxjs'; 14 | 15 | export type IncludePropType< 16 | T, 17 | V, 18 | WithNevers = { 19 | [K in keyof T]: Exclude extends V 20 | ? T[K] extends Record 21 | ? IncludePropType 22 | : T[K] 23 | : never; 24 | } 25 | > = Prettify< 26 | Pick< 27 | WithNevers, 28 | { 29 | [K in keyof WithNevers]: WithNevers[K] extends never 30 | ? never 31 | : K extends string 32 | ? K 33 | : never; 34 | }[keyof WithNevers] 35 | > 36 | >; 37 | 38 | export type Store = Type< 39 | Record & StateSignals 40 | >; 41 | 42 | export type CreateReduxState = { 43 | [K in StoreName as `provide${Capitalize}Store`]: ( 44 | connectReduxDevtools?: boolean 45 | ) => EnvironmentProviders; 46 | } & { 47 | [K in StoreName as `inject${Capitalize}Store`]: () => InjectableReduxSlice; 48 | }; 49 | 50 | export type Selectors = IncludePropType< 51 | InstanceType, 52 | Signal | DeepSignal 53 | >; 54 | export type Dispatch = { 55 | dispatch: ( 56 | input: Action | Observable | Signal 57 | ) => Unsubscribable; 58 | }; 59 | export type InjectableReduxSlice = Selectors & 60 | Dispatch; 61 | 62 | export type ExtractActionTypes = { 63 | [Key in keyof Creators]: Creators[Key] extends ActionCreator 64 | ? T 65 | : never; 66 | }; 67 | 68 | export interface ActionMethod { 69 | (action: V): T; 70 | } 71 | 72 | export interface StoreMethod< 73 | Creators extends readonly ActionCreator[], 74 | ResultState = unknown 75 | > { 76 | (action: ActionType): ResultState; 77 | } 78 | 79 | export interface MapperTypes { 80 | types: ExtractActionTypes; 81 | storeMethod: StoreMethod; 82 | resultMethod?: (...args: unknown[]) => unknown; 83 | } 84 | 85 | export type ServiceWithDecorator = { 86 | ɵprov?: { 87 | providedIn?: string; 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/src/lib/rxjs-interop/redux-method.ts: -------------------------------------------------------------------------------- 1 | import { Injector, Signal, inject } from "@angular/core"; 2 | import { rxMethod } from "@ngrx/signals/rxjs-interop"; 3 | import { Observable, Unsubscribable, map, pipe } from "rxjs"; 4 | 5 | 6 | type RxMethodInput = Input | Observable | Signal; 7 | 8 | type RxMethodRef = { 9 | destroy: () => void; 10 | } 11 | 12 | type RxMethod = (( 13 | input: RxMethodInput, 14 | resultMethod: (input: MethodInput) => MethodResult 15 | ) => RxMethodRef) & RxMethodRef; 16 | 17 | export function reduxMethod( 18 | generator: (source$: Observable) => Observable, 19 | config?: { injector?: Injector } 20 | ): RxMethod; 21 | export function reduxMethod( 22 | generator: (source$: Observable) => Observable, 23 | resultMethod: (input: MethodInput) => MethodResult, 24 | config?: { 25 | injector?: Injector 26 | } 27 | ): RxMethod; 28 | export function reduxMethod( 29 | generator: (source$: Observable) => Observable, 30 | resultMethodOrConfig?: ((input: MethodInput) => MethodResult) | { 31 | injector?: Injector 32 | }, 33 | config?: { 34 | injector?: Injector 35 | } 36 | ): RxMethod { 37 | const injector = inject(Injector); 38 | 39 | if (typeof resultMethodOrConfig === 'function') { 40 | let unsubscribable: Unsubscribable; 41 | const inputResultFn = (( 42 | input: RxMethodInput, 43 | resultMethod = resultMethodOrConfig 44 | ) => { 45 | 46 | const rxMethodWithResult = rxMethod(pipe( 47 | generator, 48 | map(resultMethod) 49 | ), { 50 | ...(config || {}), 51 | injector: config?.injector || injector 52 | }); 53 | const rxWithInput = rxMethodWithResult(input); 54 | unsubscribable = { unsubscribe: rxWithInput.destroy.bind(rxWithInput) }; 55 | 56 | return rxWithInput; 57 | }) as RxMethod; 58 | 59 | inputResultFn.destroy = () => unsubscribable?.unsubscribe(); 60 | 61 | return inputResultFn; 62 | } 63 | 64 | return rxMethod(generator, resultMethodOrConfig); 65 | } 66 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/src/lib/signal-redux-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, inject } from "@angular/core"; 2 | import { rxMethod } from "@ngrx/signals/rxjs-interop"; 3 | import { Action, ActionCreator } from "@ngrx/store"; 4 | import { pipe, tap } from "rxjs"; 5 | import { MapperTypes } from "./model"; 6 | import { isUnsubscribable } from "./util"; 7 | 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class SignalReduxStore { 13 | private mapperDict: Record unknown, 15 | resultMethod?: (...args: unknown[]) => unknown, 16 | }> = {}; 17 | 18 | dispatch = rxMethod(pipe( 19 | tap((action: Action) => { 20 | const callbacks = this.mapperDict[action.type]; 21 | if (callbacks?.storeMethod) { 22 | if ( 23 | isUnsubscribable(callbacks.storeMethod) && 24 | callbacks.resultMethod 25 | ) { 26 | return callbacks.storeMethod(action, (a: Action) => { 27 | const resultAction = callbacks.resultMethod?.(a) as Action; 28 | this.dispatch(resultAction); 29 | }); 30 | } 31 | 32 | return callbacks?.storeMethod(action); 33 | } 34 | 35 | return; 36 | }) 37 | )); 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | connectFeatureStore(mappers: MapperTypes[]>[]): void { 41 | mappers.forEach( 42 | mapper => mapper.types.forEach( 43 | action => this.mapperDict[action] = { 44 | storeMethod: mapper.storeMethod, 45 | resultMethod: mapper.resultMethod 46 | } 47 | ) 48 | ); 49 | } 50 | } 51 | 52 | export function injectReduxDispatch() { 53 | return inject(SignalReduxStore).dispatch; 54 | } 55 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/redux-connector/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from '@ngrx/store'; 2 | import { Unsubscribable } from 'rxjs'; 3 | 4 | export function isUnsubscribable unknown>( 5 | fn: F | (F & Unsubscribable) 6 | ): fn is F & Unsubscribable { 7 | return !!(fn as F & Unsubscribable)?.unsubscribe; 8 | } 9 | 10 | export function capitalize(str: string): string { 11 | return str ? str[0].toUpperCase() + str.substring(1) : str; 12 | } 13 | 14 | export function isActionCreator(action: unknown): action is ActionCreator { 15 | return Boolean( 16 | typeof action === 'function' && 17 | action && 18 | 'type' in action && 19 | action.type && 20 | typeof action.type === 'string' 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/index.ts: -------------------------------------------------------------------------------- 1 | export { withDevToolsStub } from './lib/devtools/with-dev-tools-stub'; 2 | export { withDevtools } from './lib/devtools/with-devtools'; 3 | export { withDisabledNameIndices } from './lib/devtools/features/with-disabled-name-indicies'; 4 | export { withMapper } from './lib/devtools/features/with-mapper'; 5 | export { withGlitchTracking } from './lib/devtools/features/with-glitch-tracking'; 6 | export { patchState, updateState } from './lib/devtools/update-state'; 7 | export { renameDevtoolsName } from './lib/devtools/rename-devtools-name'; 8 | export { 9 | provideDevtoolsConfig, 10 | ReduxDevtoolsConfig, 11 | } from './lib/devtools/provide-devtools-config'; 12 | 13 | export { 14 | withRedux, 15 | payload, 16 | noPayload, 17 | createReducer, 18 | createEffects, 19 | } from './lib/with-redux'; 20 | 21 | export * from './lib/with-call-state'; 22 | export * from './lib/with-undo-redo'; 23 | export * from './lib/with-data-service'; 24 | export * from './lib/with-pagination'; 25 | export { withReset, setResetState } from './lib/with-reset'; 26 | 27 | export { withLocalStorage } from './lib/storage-sync/features/with-local-storage'; 28 | export { withSessionStorage } from './lib/storage-sync/features/with-session-storage'; 29 | export { withIndexeddb } from './lib/storage-sync/features/with-indexeddb'; 30 | export { 31 | withStorageSync, 32 | SyncConfig, 33 | } from './lib/storage-sync/with-storage-sync'; 34 | export { withImmutableState } from './lib/immutable-state/with-immutable-state'; 35 | export { withFeatureFactory } from './lib/with-feature-factory'; 36 | export { withConditional, emptyFeature } from './lib/with-conditional'; 37 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/assertions/assertions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsFnSpecs } from '../with-redux'; 2 | 3 | export function assertActionFnSpecs( 4 | obj: unknown 5 | ): asserts obj is ActionsFnSpecs { 6 | if (!obj || typeof obj !== 'object') { 7 | throw new Error('%o is not an Action Specification'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/features/with-disabled-name-indicies.ts: -------------------------------------------------------------------------------- 1 | import { createDevtoolsFeature } from '../internal/devtools-feature'; 2 | 3 | /** 4 | * If multiple instances of the same SignalStore class 5 | * exist, their devtool names are indexed. 6 | * 7 | * For example: 8 | * 9 | * ```typescript 10 | * const Store = signalStore( 11 | * withDevtools('flights') 12 | * ) 13 | * 14 | * const store1 = new Store(); // will show up as 'flights' 15 | * const store2 = new Store(); // will show up as 'flights-1' 16 | * ``` 17 | * 18 | * With adding `withDisabledNameIndices` to the store: 19 | * ```typescript 20 | * const Store = signalStore( 21 | * withDevtools('flights', withDisabledNameIndices()) 22 | * ) 23 | * 24 | * const store1 = new Store(); // will show up as 'flights' 25 | * const store2 = new Store(); //💥 throws an error 26 | * ``` 27 | * 28 | */ 29 | export function withDisabledNameIndices() { 30 | return createDevtoolsFeature({ indexNames: false }); 31 | } 32 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/features/with-glitch-tracking.ts: -------------------------------------------------------------------------------- 1 | import { createDevtoolsFeature } from '../internal/devtools-feature'; 2 | import { GlitchTrackerService } from '../internal/glitch-tracker.service'; 3 | 4 | /** 5 | * It tracks all state changes of the State, including intermediary updates 6 | * that are typically suppressed by Angular's glitch-free mechanism. 7 | * 8 | * This feature is especially useful for debugging. 9 | * 10 | * Example: 11 | * 12 | * ```typescript 13 | * const Store = signalStore( 14 | * { providedIn: 'root' }, 15 | * withState({ count: 0 }), 16 | * withDevtools('counter', withGlitchTracking()), 17 | * withMethods((store) => ({ 18 | * increase: () => 19 | * patchState(store, (value) => ({ count: value.count + 1 })), 20 | * })) 21 | * ); 22 | * 23 | * // would show up in the DevTools with value 0 24 | * const store = inject(Store); 25 | * 26 | * store.increase(); // would show up in the DevTools with value 1 27 | * store.increase(); // would show up in the DevTools with value 2 28 | * store.increase(); // would show up in the DevTools with value 3 29 | * ``` 30 | * 31 | * Without `withGlitchTracking`, the DevTools would only show the final value of 3. 32 | */ 33 | export function withGlitchTracking() { 34 | return createDevtoolsFeature({ tracker: GlitchTrackerService }); 35 | } 36 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/features/with-mapper.ts: -------------------------------------------------------------------------------- 1 | import { createDevtoolsFeature, Mapper } from '../internal/devtools-feature'; 2 | 3 | /** 4 | * Allows you to define a function to map the state. 5 | * 6 | * It is needed for huge states, that slows down the Devtools and where 7 | * you don't need to see the whole state or other reasons. 8 | * 9 | * Example: 10 | * 11 | * ```typescript 12 | * const initialState = { 13 | * id: 1, 14 | * email: 'john.list@host.com', 15 | * name: 'John List', 16 | * enteredPassword: '' 17 | * } 18 | * 19 | * const Store = signalStore( 20 | * withState(initialState), 21 | * withDevtools( 22 | * 'user', 23 | * withMapper(state => ({...state, enteredPassword: '***' })) 24 | * ) 25 | * ) 26 | * ``` 27 | * 28 | * @param map function which maps the state 29 | */ 30 | export function withMapper( 31 | map: (state: State) => Record 32 | ) { 33 | return createDevtoolsFeature({ map: map as Mapper }); 34 | } 35 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/internal/current-action-names.ts: -------------------------------------------------------------------------------- 1 | export const currentActionNames = new Set(); 2 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/internal/default-tracker.ts: -------------------------------------------------------------------------------- 1 | import { effect, Injectable, signal } from '@angular/core'; 2 | import { getState, StateSource } from '@ngrx/signals'; 3 | import { Tracker, TrackerStores } from './models'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class DefaultTracker implements Tracker { 7 | readonly #stores = signal({}); 8 | 9 | get stores(): TrackerStores { 10 | return this.#stores(); 11 | } 12 | 13 | #trackCallback: undefined | ((changedState: Record) => void); 14 | 15 | #trackingEffect = effect(() => { 16 | if (this.#trackCallback === undefined) { 17 | throw new Error('no callback function defined'); 18 | } 19 | const stores = this.#stores(); 20 | 21 | const fullState = Object.entries(stores).reduce((acc, [id, store]) => { 22 | return { ...acc, [id]: getState(store) }; 23 | }, {} as Record); 24 | 25 | this.#trackCallback(fullState); 26 | }); 27 | 28 | track(id: string, store: StateSource): void { 29 | this.#stores.update((value) => ({ 30 | ...value, 31 | [id]: store, 32 | })); 33 | } 34 | 35 | onChange(callback: (changedState: Record) => void): void { 36 | this.#trackCallback = callback; 37 | } 38 | 39 | removeStore(id: string) { 40 | this.#stores.update((stores) => 41 | Object.entries(stores).reduce((newStore, [storeId, state]) => { 42 | if (storeId !== id) { 43 | newStore[storeId] = state; 44 | } 45 | return newStore; 46 | }, {} as TrackerStores) 47 | ); 48 | } 49 | 50 | notifyRenamedStore(id: string): void { 51 | if (this.#stores()[id]) { 52 | this.#stores.update((stores) => { 53 | return { ...stores }; 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/internal/devtools-feature.ts: -------------------------------------------------------------------------------- 1 | import { Tracker } from './models'; 2 | 3 | export const DEVTOOLS_FEATURE = Symbol('DEVTOOLS_FEATURE'); 4 | 5 | export type Mapper = (state: object) => object; 6 | 7 | export type DevtoolsOptions = { 8 | indexNames?: boolean; // defines if names should be indexed. 9 | map?: Mapper; // defines a mapper for the state. 10 | tracker?: new () => Tracker; // defines a tracker for the state 11 | }; 12 | 13 | export type DevtoolsInnerOptions = { 14 | indexNames: boolean; 15 | map: Mapper; 16 | tracker: Tracker; 17 | }; 18 | 19 | /** 20 | * A DevtoolsFeature adds or modifies the behavior of the 21 | * devtools extension. 22 | * 23 | * We use them (function calls) instead of a config object, 24 | * because of tree-shaking. 25 | */ 26 | export type DevtoolsFeature = { 27 | [DEVTOOLS_FEATURE]: true; 28 | } & Partial; 29 | 30 | export function createDevtoolsFeature( 31 | options: DevtoolsOptions 32 | ): DevtoolsFeature { 33 | return { 34 | [DEVTOOLS_FEATURE]: true, 35 | ...options, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/internal/glitch-tracker.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Tracker, TrackerStores } from './models'; 3 | import { getState, StateSource, watchState } from '@ngrx/signals'; 4 | import { throwIfNull } from '../../shared/throw-if-null'; 5 | 6 | type Stores = Record< 7 | string, 8 | { destroyWatcher: () => void; store: StateSource } 9 | >; 10 | 11 | /** 12 | * Internal Service used by {@link withGlitchTracking}. It does not rely 13 | * on `effect` as {@link DefaultTracker} does but uses the NgRx function 14 | * `watchState` to track all state changes. 15 | */ 16 | @Injectable({ providedIn: 'root' }) 17 | export class GlitchTrackerService implements Tracker { 18 | #stores: Stores = {}; 19 | #callback: ((changedState: Record) => void) | undefined; 20 | 21 | get stores() { 22 | return Object.entries(this.#stores).reduce((acc, [id, { store }]) => { 23 | acc[id] = store; 24 | return acc; 25 | }, {} as TrackerStores); 26 | } 27 | 28 | onChange(callback: (changedState: Record) => void): void { 29 | this.#callback = callback; 30 | } 31 | 32 | removeStore(id: string): void { 33 | this.#stores = Object.entries(this.#stores).reduce( 34 | (newStore, [storeId, value]) => { 35 | if (storeId !== id) { 36 | newStore[storeId] = value; 37 | } else { 38 | value.destroyWatcher(); 39 | } 40 | return newStore; 41 | }, 42 | {} as Stores 43 | ); 44 | 45 | throwIfNull(this.#callback)({}); 46 | } 47 | 48 | track(id: string, store: StateSource): void { 49 | const watcher = watchState(store, (state) => { 50 | throwIfNull(this.#callback)({ [id]: state }); 51 | }); 52 | 53 | this.#stores[id] = { destroyWatcher: watcher.destroy, store }; 54 | } 55 | 56 | notifyRenamedStore(id: string): void { 57 | if (Object.keys(this.#stores).includes(id) && this.#callback) { 58 | this.#callback({ [id]: getState(this.#stores[id].store) }); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/internal/models.ts: -------------------------------------------------------------------------------- 1 | import { StateSource } from '@ngrx/signals'; 2 | import { DevtoolsInnerOptions } from './devtools-feature'; 3 | import { ReduxDevtoolsConfig } from '../provide-devtools-config'; 4 | 5 | export type Action = { type: string }; 6 | export type Connection = { 7 | send: (action: Action, state: Record) => void; 8 | }; 9 | export type ReduxDevtoolsExtension = { 10 | connect: (options: ReduxDevtoolsConfig) => Connection; 11 | }; 12 | 13 | export type StoreRegistry = Record< 14 | string, 15 | { 16 | options: DevtoolsInnerOptions; 17 | name: string; 18 | } 19 | >; 20 | 21 | export type Tracker = { 22 | track(id: string, store: StateSource): void; 23 | onChange(callback: (changedState: Record) => void): void; 24 | notifyRenamedStore(id: string): void; 25 | removeStore(id: string): void; 26 | get stores(): TrackerStores; 27 | }; 28 | 29 | export type TrackerStores = Record>; 30 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/provide-devtools-config.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, ValueProvider } from '@angular/core'; 2 | 3 | /** 4 | * Provides the configuration options for connecting to the Redux DevTools Extension. 5 | */ 6 | export function provideDevtoolsConfig( 7 | config: ReduxDevtoolsConfig 8 | ): ValueProvider { 9 | return { 10 | provide: REDUX_DEVTOOLS_CONFIG, 11 | useValue: config, 12 | }; 13 | } 14 | 15 | /** 16 | * Injection token for the configuration options for connecting to the Redux DevTools Extension. 17 | */ 18 | export const REDUX_DEVTOOLS_CONFIG = new InjectionToken( 19 | 'ReduxDevtoolsConfig' 20 | ); 21 | 22 | /** 23 | * Options for connecting to the Redux DevTools Extension. 24 | * @example 25 | * const devToolsOptions: ReduxDevtoolsConfig = { 26 | * name: 'My App', 27 | * }; 28 | */ 29 | export type ReduxDevtoolsConfig = { 30 | /** Optional name for the devtools instance. If empty, "NgRx SignalStore" will be used. */ 31 | name?: string; 32 | }; 33 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/rename-devtools-name.ts: -------------------------------------------------------------------------------- 1 | import { StateSource } from '@ngrx/signals'; 2 | import { renameDevtoolsMethodName } from './with-devtools'; 3 | 4 | /** 5 | * Renames the name of a store how it appears in the Devtools. 6 | * @param store instance of the SignalStore 7 | * @param newName new name for the Devtools 8 | */ 9 | export function renameDevtoolsName( 10 | store: StateSource, 11 | newName: string 12 | ): void { 13 | const renameMethod = (store as Record void>)[ 14 | renameDevtoolsMethodName 15 | ]; 16 | if (!renameMethod) { 17 | throw new Error("Devtools extensions haven't been added to this store."); 18 | } 19 | 20 | renameMethod(newName); 21 | } 22 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/action-name.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupExtensions } from './helpers.spec'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { signalStore, withMethods, withState } from '@ngrx/signals'; 4 | import { withDevtools } from '../with-devtools'; 5 | import { updateState } from '../update-state'; 6 | 7 | describe('updateState', () => { 8 | it('should show the name of the action', () => { 9 | const { sendSpy } = setupExtensions(); 10 | TestBed.inject( 11 | signalStore( 12 | { providedIn: 'root' }, 13 | withDevtools('shop'), 14 | withState({ name: 'Car' }) 15 | ) 16 | ); 17 | TestBed.flushEffects(); 18 | expect(sendSpy).toHaveBeenCalledWith( 19 | { type: 'Store Update' }, 20 | { shop: { name: 'Car' } } 21 | ); 22 | }); 23 | 24 | it('should set the action name', () => { 25 | const { sendSpy } = setupExtensions(); 26 | 27 | const Store = signalStore( 28 | { providedIn: 'root' }, 29 | withDevtools('shop'), 30 | withState({ name: 'Car' }), 31 | withMethods((store) => ({ 32 | setName(name: string) { 33 | updateState(store, 'Set Name', { name }); 34 | }, 35 | })) 36 | ); 37 | const store = TestBed.inject(Store); 38 | TestBed.flushEffects(); 39 | 40 | store.setName('i4'); 41 | TestBed.flushEffects(); 42 | 43 | expect(sendSpy).lastCalledWith( 44 | { type: 'Set Name' }, 45 | { shop: { name: 'i4' } } 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/connecting.spec.ts: -------------------------------------------------------------------------------- 1 | import { signalStore } from '@ngrx/signals'; 2 | import { withDevtools } from '../with-devtools'; 3 | import { setupExtensions } from './helpers.spec'; 4 | import { TestBed } from '@angular/core/testing'; 5 | 6 | describe('connect & send', () => { 7 | it('should connect', () => { 8 | const Store = signalStore({ providedIn: 'root' }, withDevtools('flight')); 9 | const { connectSpy } = setupExtensions(); 10 | TestBed.inject(Store); 11 | expect(connectSpy).toHaveBeenCalledTimes(1); 12 | }); 13 | 14 | it('should not connect if Redux Devtools are not available', () => { 15 | const { connectSpy } = setupExtensions(true, false); 16 | expect(connectSpy).toHaveBeenCalledTimes(0); 17 | }); 18 | 19 | it('should not throw if it runs on the server', () => { 20 | setupExtensions(true, false); 21 | const Store = signalStore({ providedIn: 'root' }, withDevtools('flight')); 22 | expect(() => TestBed.inject(Store)).not.toThrowError(); 23 | }); 24 | 25 | it('should only send when store is initialized', () => { 26 | const { sendSpy } = setupExtensions(); 27 | expect(sendSpy).toHaveBeenCalledTimes(0); 28 | 29 | const Store = signalStore({ providedIn: 'root' }, withDevtools('flight')); 30 | TestBed.flushEffects(); 31 | expect(sendSpy).toHaveBeenCalledTimes(0); 32 | 33 | TestBed.inject(Store); 34 | TestBed.flushEffects(); 35 | expect(sendSpy).toHaveBeenCalledTimes(1); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { PLATFORM_ID } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | export type Flight = { 5 | id: number; 6 | from: string; 7 | to: string; 8 | date: Date; 9 | delayed: boolean; 10 | }; 11 | 12 | export function setupExtensions( 13 | isPlatformBrowser = true, 14 | isExtensionAvailable = true 15 | ) { 16 | const sendSpy = jest.fn(); 17 | const connection = { 18 | send: sendSpy, 19 | }; 20 | const connectSpy = jest.fn(() => connection); 21 | 22 | if (isExtensionAvailable) { 23 | window.__REDUX_DEVTOOLS_EXTENSION__ = { connect: connectSpy }; 24 | } 25 | 26 | if (isPlatformBrowser) { 27 | TestBed.configureTestingModule({ 28 | providers: [ 29 | { 30 | provide: PLATFORM_ID, 31 | useValue: isPlatformBrowser ? 'browser' : 'server', 32 | }, 33 | ], 34 | }); 35 | } 36 | 37 | return { sendSpy, connectSpy }; 38 | } 39 | 40 | it('should initialize', () => { 41 | const { connectSpy } = setupExtensions(); 42 | expect(connectSpy).not.toHaveBeenCalled(); 43 | }); 44 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/provide-devtools-config.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { provideDevtoolsConfig } from '../provide-devtools-config'; 3 | import { DevtoolsSyncer } from '../internal/devtools-syncer.service'; 4 | import { setupExtensions } from './helpers.spec'; 5 | 6 | describe('provideDevtoolsConfig', () => { 7 | it('DevtoolsSyncer should use the default configuration if none is provided', () => { 8 | const { connectSpy } = setupExtensions(); 9 | TestBed.inject(DevtoolsSyncer); 10 | expect(connectSpy).toHaveBeenCalledWith({ 11 | name: 'NgRx SignalStore', 12 | }); 13 | }); 14 | 15 | it('DevtoolsSyncer should use the configuration provided', () => { 16 | const { connectSpy } = setupExtensions(); 17 | TestBed.configureTestingModule({ 18 | providers: [provideDevtoolsConfig({ name: 'test' })], 19 | }); 20 | TestBed.inject(DevtoolsSyncer); 21 | expect(connectSpy).toHaveBeenCalledWith({ 22 | name: 'test', 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/types.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '@angular/core'; 2 | import { patchState, signalStore, withState } from '@ngrx/signals'; 3 | import { withDevtools } from '../with-devtools'; 4 | 5 | it('should compile when signalStore is extended from', () => { 6 | class CounterStore extends signalStore( 7 | { protectedState: false }, 8 | withState({ count: 0 }), 9 | withDevtools('counter-store') 10 | ) { 11 | readonly myReadonlyProp = 42; 12 | 13 | readonly doubleCount = computed(() => this.count() * 2); 14 | 15 | increment(): void { 16 | patchState(this, { count: this.count() + 1 }); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/with-devtools.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Devtools', () => { 2 | it.todo('should group multiple patchStates (glitch-free) in one action'); 3 | it.todo('should allow time-travel (revert state via devtools'); 4 | it.todo('should clear actionNames automatically onDestroy'); 5 | }); 6 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/tests/with-mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupExtensions } from './helpers.spec'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { signalStore, withState } from '@ngrx/signals'; 4 | import { withMapper } from '../features/with-mapper'; 5 | import { withDevtools } from '../with-devtools'; 6 | 7 | function domRemover(state: Record) { 8 | return Object.keys(state).reduce((acc, key) => { 9 | const value = state[key]; 10 | 11 | if (value instanceof HTMLElement) { 12 | return acc; 13 | } else { 14 | return { ...acc, [key]: value }; 15 | } 16 | }, {}); 17 | } 18 | 19 | describe('with-mapper', () => { 20 | it('should remove DOM Nodes', () => { 21 | const { sendSpy } = setupExtensions(); 22 | 23 | const Store = signalStore( 24 | { providedIn: 'root' }, 25 | withState({ 26 | name: 'Car', 27 | carElement: document.createElement('div'), 28 | }), 29 | withDevtools('shop', withMapper(domRemover)) 30 | ); 31 | 32 | TestBed.inject(Store); 33 | TestBed.flushEffects(); 34 | expect(sendSpy).toHaveBeenCalledWith( 35 | { type: 'Store Update' }, 36 | { shop: { name: 'Car' } } 37 | ); 38 | }); 39 | 40 | it('should every property ending with *Key', () => { 41 | const { sendSpy } = setupExtensions(); 42 | const Store = signalStore( 43 | { providedIn: 'root' }, 44 | withState({ 45 | name: 'Car', 46 | unlockKey: '1234', 47 | }), 48 | withDevtools( 49 | 'shop', 50 | withMapper((state: Record) => 51 | Object.keys(state).reduce((acc, key) => { 52 | if (key.endsWith('Key')) { 53 | return acc; 54 | } else { 55 | return { ...acc, [key]: state[key] }; 56 | } 57 | }, {}) 58 | ) 59 | ) 60 | ); 61 | 62 | TestBed.inject(Store); 63 | TestBed.flushEffects(); 64 | expect(sendSpy).toHaveBeenCalledWith( 65 | { type: 'Store Update' }, 66 | { shop: { name: 'Car' } } 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/update-state.ts: -------------------------------------------------------------------------------- 1 | import { patchState as originalPatchState } from '@ngrx/signals'; 2 | import { PartialStateUpdater, WritableStateSource } from '@ngrx/signals'; 3 | import { Prettify } from '../shared/prettify'; 4 | import { currentActionNames } from './internal/current-action-names'; 5 | 6 | type PatchFn = typeof originalPatchState extends ( 7 | arg1: infer First, 8 | ...args: infer Rest 9 | ) => infer Returner 10 | ? (state: First, action: string, ...rest: Rest) => Returner 11 | : never; 12 | 13 | /** 14 | * @deprecated Has been renamed to `updateState` 15 | */ 16 | export const patchState: PatchFn = (state, action, ...rest) => { 17 | updateState(state, action, ...rest); 18 | }; 19 | 20 | /** 21 | * Wrapper of `patchState` for DevTools integration. Next to updating the state, 22 | * it also sends the action to the DevTools. 23 | * @param stateSource state of Signal Store 24 | * @param action name of action how it will show in DevTools 25 | * @param updaters updater functions or objects 26 | */ 27 | export function updateState( 28 | stateSource: WritableStateSource, 29 | action: string, 30 | ...updaters: Array< 31 | Partial> | PartialStateUpdater> 32 | > 33 | ): void { 34 | currentActionNames.add(action); 35 | return originalPatchState(stateSource, ...updaters); 36 | } 37 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/with-dev-tools-stub.ts: -------------------------------------------------------------------------------- 1 | import { withDevtools } from './with-devtools'; 2 | 3 | /** 4 | * Stub for DevTools integration. Can be used to disable DevTools in production. 5 | */ 6 | export const withDevToolsStub: typeof withDevtools = () => (store) => store; 7 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/devtools/with-devtools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmptyFeatureResult, 3 | SignalStoreFeature, 4 | signalStoreFeature, 5 | withHooks, 6 | withMethods, 7 | } from '@ngrx/signals'; 8 | import { inject, InjectionToken } from '@angular/core'; 9 | import { DevtoolsSyncer } from './internal/devtools-syncer.service'; 10 | import { 11 | DevtoolsFeature, 12 | DevtoolsInnerOptions, 13 | } from './internal/devtools-feature'; 14 | import { DefaultTracker } from './internal/default-tracker'; 15 | import { ReduxDevtoolsExtension } from './internal/models'; 16 | 17 | declare global { 18 | interface Window { 19 | __REDUX_DEVTOOLS_EXTENSION__: ReduxDevtoolsExtension | undefined; 20 | } 21 | } 22 | 23 | export const renameDevtoolsMethodName = '___renameDevtoolsName'; 24 | export const uniqueDevtoolsId = '___uniqueDevtoolsId'; 25 | 26 | const EXISTING_NAMES = new InjectionToken( 27 | 'Array contain existing names for the signal stores', 28 | { factory: () => [] as string[], providedIn: 'root' } 29 | ); 30 | 31 | /** 32 | * Adds this store as a feature state to the Redux DevTools. 33 | * 34 | * By default, the action name is 'Store Update'. You can 35 | * change that via the {@link updateState} method, which has as second 36 | * parameter the action name. 37 | * 38 | * The standalone function {@link renameDevtoolsName} can rename 39 | * the store name. 40 | * 41 | * @param name name of the store as it should appear in the DevTools 42 | * @param features features to extend or modify the behavior of the Devtools 43 | */ 44 | export function withDevtools(name: string, ...features: DevtoolsFeature[]) { 45 | return signalStoreFeature( 46 | withMethods(() => { 47 | const syncer = inject(DevtoolsSyncer); 48 | 49 | const id = syncer.getNextId(); 50 | 51 | // TODO: use withProps and symbols 52 | return { 53 | [renameDevtoolsMethodName]: (newName: string) => { 54 | syncer.renameStore(name, newName); 55 | }, 56 | [uniqueDevtoolsId]: () => id, 57 | } as Record unknown>; 58 | }), 59 | withHooks((store) => { 60 | const syncer = inject(DevtoolsSyncer); 61 | const id = String(store[uniqueDevtoolsId]()); 62 | return { 63 | onInit() { 64 | const id = String(store[uniqueDevtoolsId]()); 65 | const finalOptions: DevtoolsInnerOptions = { 66 | indexNames: !features.some((f) => f.indexNames === false), 67 | map: features.find((f) => f.map)?.map ?? ((state) => state), 68 | tracker: inject( 69 | features.find((f) => f.tracker)?.tracker || DefaultTracker 70 | ), 71 | }; 72 | 73 | syncer.addStore(id, name, store, finalOptions); 74 | }, 75 | onDestroy() { 76 | syncer.removeStore(id); 77 | }, 78 | }; 79 | }) 80 | ) as SignalStoreFeature; 81 | } 82 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/immutable-state/deep-freeze.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deep freezes a state object along its properties with primitive values 3 | * on the first level. 4 | * 5 | * The reason for this is that the final state is a merge of all 6 | * root properties of all states, i.e. `withState`,.... 7 | * 8 | * Since the root object will not be part of the state (shadow clone), 9 | * we are not freezing it. 10 | */ 11 | 12 | export function deepFreeze>( 13 | target: T, 14 | // if empty all properties will be frozen 15 | propertyNamesToBeFrozen: (string | symbol)[], 16 | // also means that we are on the first level 17 | isRoot = true 18 | ): void { 19 | const runPropertyNameCheck = propertyNamesToBeFrozen.length > 0; 20 | for (const key of Reflect.ownKeys(target)) { 21 | if (runPropertyNameCheck && !propertyNamesToBeFrozen.includes(key)) { 22 | continue; 23 | } 24 | 25 | const propValue = target[key]; 26 | if (isRecordLike(propValue) && !Object.isFrozen(propValue)) { 27 | Object.freeze(propValue); 28 | deepFreeze(propValue, [], false); 29 | } else if (isRoot) { 30 | Object.defineProperty(target, key, { 31 | value: propValue, 32 | writable: false, 33 | configurable: false, 34 | }); 35 | } 36 | } 37 | } 38 | 39 | function isRecordLike( 40 | target: unknown 41 | ): target is Record { 42 | return typeof target === 'object' && target !== null; 43 | } 44 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/immutable-state/is-dev-mode.ts: -------------------------------------------------------------------------------- 1 | import { isDevMode as ngIsInDevMode } from '@angular/core'; 2 | 3 | // necessary wrapper function to test prod mode 4 | export function isDevMode() { 5 | return ngIsInDevMode(); 6 | } 7 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/shared/prettify.ts: -------------------------------------------------------------------------------- 1 | export type Prettify = { 2 | [Key in keyof Type]: Type[Key]; 3 | }; 4 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/shared/signal-store-models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains copies of types of the Signal Store which are not public. 3 | * 4 | * Since certain features depend on them, if we don't want to break 5 | * the encapsulation of @ngrx/signals, we decided to copy them. 6 | * 7 | * Since TypeScript is based on structural typing, we can get away with it. 8 | * 9 | * If @ngrx/signals changes its internal types, we catch them via integration 10 | * tests. 11 | * 12 | * Because of the "tight coupling", the toolkit doesn't have version range 13 | * to @ngrx/signal, but is very precise. 14 | */ 15 | import { Signal } from '@angular/core'; 16 | import { EntityId } from '@ngrx/signals/entities'; 17 | 18 | // withEntites models 19 | export type EntityState = { 20 | entityMap: Record; 21 | ids: EntityId[]; 22 | }; 23 | 24 | export type EntityComputed = { 25 | entities: Signal; 26 | }; 27 | 28 | export type NamedEntityComputed = { 29 | [K in keyof EntityComputed as `${Collection}${Capitalize}`]: EntityComputed[K]; 30 | }; 31 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/shared/throw-if-null.ts: -------------------------------------------------------------------------------- 1 | export function throwIfNull(obj: T): NonNullable { 2 | if (obj === null || obj === undefined) { 3 | throw new Error(''); 4 | } 5 | 6 | return obj; 7 | } 8 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/features/with-indexeddb.ts: -------------------------------------------------------------------------------- 1 | import { IndexedDBService } from '../internal/indexeddb.service'; 2 | 3 | export const withIndexeddb = () => IndexedDBService; 4 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/features/with-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorageService } from '../internal/local-storage.service'; 2 | 3 | export const withLocalStorage = () => LocalStorageService; 4 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/features/with-session-storage.ts: -------------------------------------------------------------------------------- 1 | import { SessionStorageService } from '../internal/session-storage.service'; 2 | 3 | export const withSessionStorage = () => SessionStorageService; 4 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/internal/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NOOP, StorageService, WithStorageSyncFeatureResult } from './models'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class LocalStorageService implements StorageService { 8 | getItem(key: string): string | null { 9 | return localStorage.getItem(key); 10 | } 11 | 12 | setItem(key: string, data: string): void { 13 | return localStorage.setItem(key, data); 14 | } 15 | 16 | clear(key: string): void { 17 | return localStorage.removeItem(key); 18 | } 19 | 20 | /** return stub */ 21 | getStub(): Pick['methods'] { 22 | return { 23 | clearStorage: NOOP, 24 | readFromStorage: NOOP, 25 | writeToStorage: NOOP, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/internal/models.ts: -------------------------------------------------------------------------------- 1 | import { EmptyFeatureResult } from '@ngrx/signals'; 2 | import { Type } from '@angular/core'; 3 | 4 | export interface StorageService { 5 | clear(key: string): void; 6 | 7 | getItem(key: string): string | null; 8 | 9 | setItem(key: string, data: string): void; 10 | 11 | getStub(): Pick['methods']; 12 | } 13 | 14 | export interface IndexeddbService { 15 | clear(key: string): Promise; 16 | 17 | getItem(key: string): Promise; 18 | 19 | setItem(key: string, data: string): Promise; 20 | 21 | getStub(): Pick['methods']; 22 | } 23 | 24 | export type StorageServiceFactory = 25 | | Type 26 | | Type; 27 | 28 | export type WithIndexeddbSyncFeatureResult = EmptyFeatureResult & { 29 | methods: { 30 | clearStorage(): Promise; 31 | readFromStorage(): Promise; 32 | writeToStorage(): Promise; 33 | }; 34 | }; 35 | 36 | export type WithStorageSyncFeatureResult = EmptyFeatureResult & { 37 | methods: { 38 | clearStorage(): void; 39 | readFromStorage(): void; 40 | writeToStorage(): void; 41 | }; 42 | }; 43 | 44 | export const NOOP = () => void true; 45 | 46 | export const PROMISE_NOOP = () => Promise.resolve(); 47 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/internal/session-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NOOP, StorageService, WithStorageSyncFeatureResult } from './models'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class SessionStorageService implements StorageService { 8 | getItem(key: string): string | null { 9 | return sessionStorage.getItem(key); 10 | } 11 | 12 | setItem(key: string, data: string): void { 13 | return sessionStorage.setItem(key, data); 14 | } 15 | 16 | clear(key: string): void { 17 | return sessionStorage.removeItem(key); 18 | } 19 | 20 | /** return stub */ 21 | getStub(): Pick['methods'] { 22 | return { 23 | clearStorage: NOOP, 24 | readFromStorage: NOOP, 25 | writeToStorage: NOOP, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/storage-sync/tests/indexeddb.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { IndexedDBService } from '../internal/indexeddb.service'; 2 | 3 | describe('IndexedDBService', () => { 4 | const sampleData = JSON.stringify({ 5 | foo: 'bar', 6 | users: [ 7 | { name: 'John', age: 30, isAdmin: true }, 8 | { name: 'Jane', age: 25, isAdmin: false }, 9 | ], 10 | }); 11 | 12 | let indexedDBService: IndexedDBService; 13 | 14 | beforeEach(() => { 15 | indexedDBService = new IndexedDBService(); 16 | }); 17 | 18 | it('It should be possible to write data using write() and then read the data using read()', async (): Promise => { 19 | const key = 'users'; 20 | 21 | const expectedData = sampleData; 22 | 23 | await indexedDBService.setItem(key, sampleData); 24 | 25 | const receivedData = await indexedDBService.getItem(key); 26 | 27 | expect(receivedData).toEqual(expectedData); 28 | }); 29 | 30 | it('It should be possible to delete data using clear()', async (): Promise => { 31 | const key = 'sample'; 32 | 33 | await indexedDBService.setItem(key, sampleData); 34 | 35 | await indexedDBService.clear(key); 36 | 37 | const receivedData = await indexedDBService.getItem(key); 38 | 39 | expect(receivedData).toEqual(null); 40 | }); 41 | 42 | it('When there is no data, read() should return null', async (): Promise => { 43 | const key = 'nullData'; 44 | 45 | const receivedData = await indexedDBService.getItem(key); 46 | 47 | expect(receivedData).toEqual(null); 48 | }); 49 | 50 | it('write() should handle null data', async (): Promise => { 51 | const key = 'nullData'; 52 | 53 | await indexedDBService.setItem(key, JSON.stringify(null)); 54 | 55 | const receivedData = await indexedDBService.getItem(key); 56 | 57 | expect(receivedData).toEqual('null'); 58 | }); 59 | 60 | it('write() should handle empty object data', async (): Promise => { 61 | const key = 'emptyData'; 62 | 63 | const emptyData = JSON.stringify({}); 64 | const expectedData = emptyData; 65 | 66 | await indexedDBService.setItem(key, emptyData); 67 | 68 | const receivedData = await indexedDBService.getItem(key); 69 | 70 | expect(receivedData).toEqual(expectedData); 71 | }); 72 | 73 | it('write() should handle large data objects', async (): Promise => { 74 | const key = 'largeData'; 75 | 76 | const largeData = JSON.stringify({ foo: 'a'.repeat(100000) }); 77 | const expectedData = largeData; 78 | 79 | await indexedDBService.setItem(key, largeData); 80 | 81 | const receivedData = await indexedDBService.getItem(key); 82 | 83 | expect(receivedData).toEqual(expectedData); 84 | }); 85 | 86 | it('write() should handle special characters in data', async (): Promise => { 87 | const key = 'specialCharData'; 88 | 89 | const specialCharData = JSON.stringify({ foo: 'bar!@#$%^&*()_+{}:"<>?' }); 90 | const expectedData = specialCharData; 91 | 92 | await indexedDBService.setItem(key, specialCharData); 93 | 94 | const receivedData = await indexedDBService.getItem(key); 95 | 96 | expect(receivedData).toEqual(expectedData); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-call-state.spec.ts: -------------------------------------------------------------------------------- 1 | import { patchState, signalStore } from '@ngrx/signals'; 2 | import { setLoaded, setLoading, withCallState } from './with-call-state'; 3 | 4 | describe('withCallState', () => { 5 | it('should use and update a callState', () => { 6 | const DataStore = signalStore({ protectedState: false }, withCallState()); 7 | const dataStore = new DataStore(); 8 | 9 | patchState(dataStore, setLoading()); 10 | 11 | expect(dataStore.callState()).toBe('loading'); 12 | expect(dataStore.loading()).toBe(true); 13 | }); 14 | 15 | it('should use the callState for a collection', () => { 16 | const DataStore = signalStore( 17 | { protectedState: false }, 18 | withCallState({ collection: 'entities' }) 19 | ); 20 | const dataStore = new DataStore(); 21 | 22 | patchState(dataStore, setLoaded('entities')); 23 | 24 | expect(dataStore.entitiesCallState()).toBe('loaded'); 25 | expect(dataStore.entitiesLoaded()).toBe(true); 26 | }); 27 | 28 | it('should use the callState for multiple collections with an array', () => { 29 | const DataStore = signalStore( 30 | { protectedState: false }, 31 | withCallState({ collections: ['entities', 'products'] }) 32 | ); 33 | const dataStore = new DataStore(); 34 | 35 | patchState(dataStore, setLoaded('entities'), setLoaded('products')); 36 | 37 | expect(dataStore.entitiesCallState()).toBe('loaded'); 38 | expect(dataStore.productsCallState()).toBe('loaded'); 39 | expect(dataStore.entitiesLoaded()).toBe(true); 40 | expect(dataStore.productsLoaded()).toBe(true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-conditional.ts: -------------------------------------------------------------------------------- 1 | import { 2 | signalStoreFeature, 3 | SignalStoreFeature, 4 | SignalStoreFeatureResult, 5 | StateSignals, 6 | withState, 7 | } from '@ngrx/signals'; 8 | 9 | /** 10 | * `withConditional` activates a feature based on a given condition. 11 | * 12 | * **Use Cases** 13 | * - Conditionally activate features based on the **store state** or other criteria. 14 | * - Choose between **two different implementations** of a feature. 15 | * 16 | * **Type Constraints** 17 | * Both features must have **exactly the same state, props, and methods**. 18 | * Otherwise, a type error will occur. 19 | * 20 | * 21 | * **Usage** 22 | * 23 | * ```typescript 24 | * const withUser = signalStoreFeature( 25 | * withState({ id: 1, name: 'Konrad' }), 26 | * withHooks(store => ({ 27 | * onInit() { 28 | * // user loading logic 29 | * } 30 | * })) 31 | * ); 32 | * 33 | * function withFakeUser() { 34 | * return signalStoreFeature( 35 | * withState({ id: 0, name: 'anonymous' }) 36 | * ); 37 | * } 38 | * 39 | * signalStore( 40 | * withMethods(() => ({ 41 | * useRealUser: () => true 42 | * })), 43 | * withConditional((store) => store.useRealUser(), withUser, withFakeUser) 44 | * ) 45 | * ``` 46 | * 47 | * @param condition - A function that determines which feature to activate based on the store state. 48 | * @param featureIfTrue - The feature to activate if the condition evaluates to `true`. 49 | * @param featureIfFalse - The feature to activate if the condition evaluates to `false`. 50 | * @returns A `SignalStoreFeature` that applies the selected feature based on the condition. 51 | */ 52 | export function withConditional< 53 | Input extends SignalStoreFeatureResult, 54 | Output extends SignalStoreFeatureResult 55 | >( 56 | condition: ( 57 | store: StateSignals & Input['props'] & Input['methods'] 58 | ) => boolean, 59 | featureIfTrue: SignalStoreFeature, Output>, 60 | featureIfFalse: SignalStoreFeature, NoInfer> 61 | ): SignalStoreFeature { 62 | return (store) => { 63 | const conditionStore = { 64 | ...store['stateSignals'], 65 | ...store['props'], 66 | ...store['methods'], 67 | }; 68 | return condition(conditionStore) 69 | ? featureIfTrue(store) 70 | : featureIfFalse(store); 71 | }; 72 | } 73 | 74 | export const emptyFeature = signalStoreFeature(withState({})); 75 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-feature-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getState, 3 | patchState, 4 | signalStore, 5 | signalStoreFeature, 6 | withComputed, 7 | withMethods, 8 | withState, 9 | } from '@ngrx/signals'; 10 | import { lastValueFrom, of } from 'rxjs'; 11 | import { withFeatureFactory } from './with-feature-factory'; 12 | import { TestBed } from '@angular/core/testing'; 13 | import { computed, Signal } from '@angular/core'; 14 | 15 | type User = { 16 | id: number; 17 | name: string; 18 | }; 19 | 20 | function withMyEntity(loadMethod: (id: number) => Promise) { 21 | return signalStoreFeature( 22 | withState({ 23 | currentId: 1 as number | undefined, 24 | entity: undefined as undefined | Entity, 25 | }), 26 | withMethods((store) => ({ 27 | async load(id: number) { 28 | const entity = await loadMethod(1); 29 | patchState(store, { entity, currentId: id }); 30 | }, 31 | })) 32 | ); 33 | } 34 | 35 | describe('withFeatureFactory', () => { 36 | it('should allow a sum feature', () => { 37 | function withSum(a: Signal, b: Signal) { 38 | return signalStoreFeature( 39 | withComputed(() => ({ sum: computed(() => a() + b()) })) 40 | ); 41 | } 42 | signalStore( 43 | withState({ a: 1, b: 2 }), 44 | withFeatureFactory((store) => withSum(store.a, store.b)) 45 | ); 46 | }); 47 | 48 | it('should allow to pass elements from a SignalStore to a feature', async () => { 49 | const UserStore = signalStore( 50 | { providedIn: 'root' }, 51 | withMethods(() => ({ 52 | findById(id: number) { 53 | return of({ id: 1, name: 'Konrad' }); 54 | }, 55 | })), 56 | withFeatureFactory((store) => { 57 | const loader = (id: number) => lastValueFrom(store.findById(id)); 58 | return withMyEntity(loader); 59 | }) 60 | ); 61 | 62 | const userStore = TestBed.inject(UserStore); 63 | await userStore.load(1); 64 | expect(getState(userStore)).toEqual({ 65 | currentId: 1, 66 | entity: { id: 1, name: 'Konrad' }, 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-feature-factory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SignalStoreFeature, 3 | SignalStoreFeatureResult, 4 | StateSignals, 5 | } from '@ngrx/signals'; 6 | 7 | type StoreForFactory = StateSignals< 8 | Input['state'] 9 | > & 10 | Input['props'] & 11 | Input['methods']; 12 | 13 | /** 14 | * @deprecated Use `import { withFeature } from '@ngrx/signals'` instead, starting with `ngrx/signals` 19.1: https://ngrx.io/guide/signals/signal-store/custom-store-features#connecting-a-custom-feature-with-the-store 15 | * 16 | * Allows to pass properties, methods, or signals from a SignalStore 17 | * to a feature. 18 | * 19 | * Typically, a `signalStoreFeature` can have input constraints on 20 | * 21 | * ```typescript 22 | * function withSum(a: Signal, b: Signal) { 23 | * return signalStoreFeature( 24 | * withComputed(() => ({ 25 | * sum: computed(() => a() + b()) 26 | * })) 27 | * ); 28 | * } 29 | * 30 | * signalStore( 31 | * withState({ a: 1, b: 2 }), 32 | * withFeatureFactory((store) => withSum(store.a, store.b)) 33 | * ); 34 | * ``` 35 | * @param factoryFn 36 | */ 37 | export function withFeatureFactory< 38 | Input extends SignalStoreFeatureResult, 39 | Output extends SignalStoreFeatureResult 40 | >( 41 | factoryFn: ( 42 | store: StoreForFactory 43 | ) => SignalStoreFeature 44 | ): SignalStoreFeature { 45 | return (store) => { 46 | const storeForFactory = { 47 | ...store['stateSignals'], 48 | ...store['props'], 49 | ...store['methods'], 50 | } as StoreForFactory; 51 | 52 | const feature = factoryFn(storeForFactory); 53 | 54 | return feature(store); 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-pagination.spec.ts: -------------------------------------------------------------------------------- 1 | import { patchState, signalStore, type } from '@ngrx/signals'; 2 | import { 3 | createPageArray, 4 | gotoPage, 5 | setPageSize, 6 | withPagination, 7 | } from './with-pagination'; 8 | import { setAllEntities, withEntities } from '@ngrx/signals/entities'; 9 | 10 | type Book = { id: number; title: string; author: string }; 11 | const generateBooks = (count = 10) => { 12 | const books = [] as Book[]; 13 | for (let i = 1; i <= count; i++) { 14 | books.push({ id: i, title: `Book ${i}`, author: `Author ${i}` }); 15 | } 16 | return books; 17 | }; 18 | 19 | describe('withPagination', () => { 20 | it('should use and update a pagination', () => { 21 | const Store = signalStore( 22 | { protectedState: false }, 23 | withEntities({ entity: type() }), 24 | withPagination() 25 | ); 26 | 27 | const store = new Store(); 28 | 29 | patchState(store, setAllEntities(generateBooks(55))); 30 | expect(store.currentPage()).toBe(0); 31 | expect(store.pageCount()).toBe(6); 32 | }); 33 | 34 | it('should use and update a pagination with collection', () => { 35 | const Store = signalStore( 36 | { protectedState: false }, 37 | withEntities({ entity: type(), collection: 'books' }), 38 | withPagination({ entity: type(), collection: 'books' }) 39 | ); 40 | 41 | const store = new Store(); 42 | 43 | patchState( 44 | store, 45 | setAllEntities(generateBooks(55), { collection: 'books' }) 46 | ); 47 | 48 | patchState(store, gotoPage(5, { collection: 'books' })); 49 | expect(store.booksCurrentPage()).toBe(5); 50 | expect(store.selectedPageBooksEntities().length).toBe(5); 51 | expect(store.booksPageCount()).toBe(6); 52 | }); 53 | 54 | it('should react on enitiy changes', () => { 55 | const Store = signalStore( 56 | { protectedState: false }, 57 | withEntities({ entity: type() }), 58 | withPagination() 59 | ); 60 | 61 | const store = new Store(); 62 | 63 | patchState(store, setAllEntities(generateBooks(100))); 64 | 65 | expect(store.pageCount()).toBe(10); 66 | 67 | patchState(store, setAllEntities(generateBooks(20))); 68 | 69 | expect(store.pageCount()).toBe(2); 70 | 71 | patchState(store, setPageSize(5)); 72 | 73 | expect(store.pageCount()).toBe(4); 74 | }); 75 | 76 | describe('internal pageNavigationArray', () => { 77 | it('should return an array of page numbers', () => { 78 | const pages = createPageArray(8, 10, 500, 7); 79 | expect(pages).toEqual([ 80 | { label: 5, value: 5 }, 81 | { label: '...', value: 6 }, 82 | { label: 7, value: 7 }, 83 | { label: 8, value: 8 }, 84 | { label: 9, value: 9 }, 85 | { label: '...', value: 10 }, 86 | { label: 50, value: 50 }, 87 | ]); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-reset.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getState, 3 | patchState, 4 | signalStore, 5 | withMethods, 6 | withState, 7 | } from '@ngrx/signals'; 8 | import { setResetState, withReset } from './with-reset'; 9 | import { TestBed } from '@angular/core/testing'; 10 | import { effect } from '@angular/core'; 11 | 12 | describe('withReset', () => { 13 | const setup = () => { 14 | const initialState = { 15 | user: { id: 1, name: 'Konrad' }, 16 | address: { city: 'Vienna', zip: '1010' }, 17 | }; 18 | 19 | const Store = signalStore( 20 | withState(initialState), 21 | withReset(), 22 | withMethods((store) => ({ 23 | changeUser(id: number, name: string) { 24 | patchState(store, { user: { id, name } }); 25 | }, 26 | changeUserName(name: string) { 27 | patchState(store, (value) => ({ user: { ...value.user, name } })); 28 | }, 29 | changeAddress(city: string, zip: string) { 30 | patchState(store, { address: { city, zip } }); 31 | }, 32 | })) 33 | ); 34 | 35 | const store = TestBed.configureTestingModule({ 36 | providers: [Store], 37 | }).inject(Store); 38 | 39 | return { store, initialState }; 40 | }; 41 | 42 | it('should reset state to initial state', () => { 43 | const { store, initialState } = setup(); 44 | 45 | store.changeUser(2, 'Max'); 46 | expect(getState(store)).toMatchObject({ 47 | user: { id: 2, name: 'Max' }, 48 | }); 49 | store.resetState(); 50 | expect(getState(store)).toStrictEqual(initialState); 51 | }); 52 | 53 | it('should not fire if reset is called on unchanged state', () => { 54 | const { store } = setup(); 55 | let effectCounter = 0; 56 | TestBed.runInInjectionContext(() => { 57 | effect(() => { 58 | store.user(); 59 | effectCounter++; 60 | }); 61 | }); 62 | TestBed.flushEffects(); 63 | store.resetState(); 64 | TestBed.flushEffects(); 65 | expect(effectCounter).toBe(1); 66 | }); 67 | 68 | it('should not fire on props which are unchanged', () => { 69 | const { store } = setup(); 70 | let effectCounter = 0; 71 | TestBed.runInInjectionContext(() => { 72 | effect(() => { 73 | store.address(); 74 | effectCounter++; 75 | }); 76 | }); 77 | 78 | TestBed.flushEffects(); 79 | expect(effectCounter).toBe(1); 80 | store.changeUserName('Max'); 81 | TestBed.flushEffects(); 82 | store.changeUser(2, 'Ludwig'); 83 | TestBed.flushEffects(); 84 | expect(effectCounter).toBe(1); 85 | }); 86 | 87 | it('should be possible to change the reset state', () => { 88 | const { store } = setup(); 89 | 90 | setResetState(store, { 91 | user: { id: 2, name: 'Max' }, 92 | address: { city: 'London', zip: 'SW1' }, 93 | }); 94 | 95 | store.changeUser(3, 'Ludwig'); 96 | store.changeAddress('Paris', '75001'); 97 | 98 | store.resetState(); 99 | expect(getState(store)).toEqual({ 100 | user: { id: 2, name: 'Max' }, 101 | address: { city: 'London', zip: 'SW1' }, 102 | }); 103 | }); 104 | 105 | it('should throw on setResetState if store is not configured with withReset()', () => { 106 | const Store = signalStore({ providedIn: 'root' }, withState({})); 107 | const store = TestBed.inject(Store); 108 | expect(() => setResetState(store, {})).toThrowError( 109 | 'Cannot set reset state, since store is not configured with withReset()' 110 | ); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/lib/with-reset.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getState, 3 | patchState, 4 | signalStoreFeature, 5 | StateSource, 6 | withHooks, 7 | withMethods, 8 | withProps, 9 | } from '@ngrx/signals'; 10 | 11 | export type PublicMethods = { 12 | resetState(): void; 13 | }; 14 | 15 | /** 16 | * Adds a `resetState` method to the store, which resets the state 17 | * to the initial state. 18 | * 19 | * If you want to set a custom initial state, you can use {@link setResetState}. 20 | */ 21 | export function withReset() { 22 | return signalStoreFeature( 23 | withProps(() => ({ _resetState: { value: {} } })), 24 | withMethods((store): PublicMethods => { 25 | // workaround to TS excessive property check 26 | const methods = { 27 | resetState() { 28 | patchState(store, store._resetState.value); 29 | }, 30 | __setResetState__(state: object) { 31 | store._resetState.value = state; 32 | }, 33 | }; 34 | 35 | return methods; 36 | }), 37 | withHooks((store) => ({ 38 | onInit() { 39 | store._resetState.value = getState(store); 40 | }, 41 | })) 42 | ); 43 | } 44 | 45 | /** 46 | * Sets the reset state of the store to the given state. 47 | * 48 | * Throws an error if the store is not configured with {@link withReset}. 49 | * @param store the instance of a SignalStore 50 | * @param state the state to set as the reset state 51 | */ 52 | export function setResetState( 53 | store: StateSource, 54 | state: State 55 | ): void { 56 | if (!('__setResetState__' in store)) { 57 | throw new Error( 58 | 'Cannot set reset state, since store is not configured with withReset()' 59 | ); 60 | } 61 | (store.__setResetState__ as (state: State) => void)(state); 62 | } 63 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 2 | globalThis.ngJest = { 3 | testEnvironmentOptions: { 4 | errorOnUnknownElements: true, 5 | errorOnUnknownProperties: true, 6 | }, 7 | }; 8 | import 'jest-preset-angular/setup-jest'; 9 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ], 22 | "extends": "../../tsconfig.base.json", 23 | "angularCompilerOptions": { 24 | "enableI18nLegacyMessageIdFormat": false, 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "inlineSources": true, 8 | "types": [] 9 | }, 10 | "exclude": [ 11 | "src/**/*.spec.ts", 12 | "src/test-setup.ts", 13 | "jest.config.ts", 14 | "src/**/*.test.ts" 15 | ], 16 | "include": ["src/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/ngrx-toolkit/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/logo.ai -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-architects/ngrx-toolkit/3e5b80610e98bbb5efe2b6384639de6cacc53042/logo.png -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "cli": "nx", 5 | "version": "20.4.0-beta.1", 6 | "requires": { "@angular/core": ">=19.1.0" }, 7 | "description": "Update the @angular/cli package version to ~19.1.0.", 8 | "factory": "./src/migrations/update-20-4-0/update-angular-cli", 9 | "package": "@nx/angular", 10 | "name": "update-angular-cli-version-19-1-0" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "packageManager": "pnpm", 4 | "targetDefaults": { 5 | "build": { 6 | "cache": true, 7 | "dependsOn": ["^build"], 8 | "inputs": ["production", "^production"] 9 | }, 10 | "@nx/jest:jest": { 11 | "cache": true, 12 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 13 | "options": { 14 | "passWithNoTests": true 15 | }, 16 | "configurations": { 17 | "ci": { 18 | "ci": true, 19 | "codeCoverage": true 20 | } 21 | } 22 | }, 23 | "e2e": { 24 | "cache": true, 25 | "inputs": ["default", "^production"] 26 | }, 27 | "@nx/eslint:lint": { 28 | "cache": true, 29 | "inputs": [ 30 | "default", 31 | "{workspaceRoot}/.eslintrc.json", 32 | "{workspaceRoot}/.eslintignore", 33 | "{workspaceRoot}/eslint.config.js", 34 | "{workspaceRoot}/eslint.config.cjs" 35 | ] 36 | }, 37 | "nx-release-publish": { 38 | "dependsOn": ["build"], 39 | "options": { 40 | "packageRoot": "dist/libs/ngrx-toolkit" 41 | } 42 | } 43 | }, 44 | "namedInputs": { 45 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 46 | "production": [ 47 | "default", 48 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 49 | "!{projectRoot}/tsconfig.spec.json", 50 | "!{projectRoot}/jest.config.[jt]s", 51 | "!{projectRoot}/src/test-setup.[jt]s", 52 | "!{projectRoot}/test-setup.[jt]s", 53 | "!{projectRoot}/.eslintrc.json", 54 | "!{projectRoot}/eslint.config.js", 55 | "!{projectRoot}/eslint.config.cjs" 56 | ], 57 | "sharedGlobals": [] 58 | }, 59 | "release": { 60 | "projects": ["ngrx-toolkit"], 61 | "version": { 62 | "conventionalCommits": true 63 | }, 64 | "changelog": { 65 | "workspaceChangelog": { 66 | "createRelease": "github" 67 | } 68 | } 69 | }, 70 | "generators": { 71 | "@nx/angular:application": { 72 | "style": "css", 73 | "linter": "eslint", 74 | "unitTestRunner": "jest", 75 | "e2eTestRunner": "playwright" 76 | }, 77 | "@nx/angular:library": { 78 | "linter": "eslint", 79 | "unitTestRunner": "jest" 80 | }, 81 | "@nx/angular:component": { 82 | "style": "css" 83 | } 84 | }, 85 | "useLegacyCache": true 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngrx-toolkit/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "nx serve demo", 7 | "lint:all": "nx run-many --targets=lint", 8 | "test:all": "nx run-many --targets=test", 9 | "test:e2e": "nx e2e demo", 10 | "build:all": "nx run-many --targets=build", 11 | "verify:all": "nx run-many --targets=lint,test,build", 12 | "prepare": "husky" 13 | }, 14 | "private": true, 15 | "engines": { 16 | "node": ">=18", 17 | "pnpm": "10" 18 | }, 19 | "dependencies": { 20 | "@angular/animations": "19.1.4", 21 | "@angular/cdk": "19.1.2", 22 | "@angular/common": "19.1.4", 23 | "@angular/compiler": "19.1.4", 24 | "@angular/core": "19.1.4", 25 | "@angular/forms": "19.1.4", 26 | "@angular/material": "19.1.2", 27 | "@angular/platform-browser": "19.1.4", 28 | "@angular/platform-browser-dynamic": "19.1.4", 29 | "@angular/router": "19.1.4", 30 | "@ngrx/signals": "19.1.0", 31 | "@ngrx/store": "19.1.0", 32 | "@nx/angular": "20.4.0", 33 | "core-js": "^3.40.0", 34 | "flush-promises": "^1.0.2", 35 | "rxjs": "~7.8.0", 36 | "tslib": "^2.3.0", 37 | "zone.js": "0.15.0" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/build-angular": "19.1.5", 41 | "@angular-devkit/core": "19.1.5", 42 | "@angular-devkit/schematics": "19.1.5", 43 | "@angular/cli": "~19.1.0", 44 | "@angular/compiler-cli": "19.1.4", 45 | "@angular/language-service": "19.1.4", 46 | "@commitlint/cli": "^19.3.0", 47 | "@commitlint/config-conventional": "^19.2.2", 48 | "@eslint/eslintrc": "^2.1.1", 49 | "@eslint/js": "~8.57.0", 50 | "@nx/devkit": "20.4.0", 51 | "@nx/eslint": "20.4.0", 52 | "@nx/eslint-plugin": "20.4.0", 53 | "@nx/jest": "20.4.0", 54 | "@nx/js": "20.4.0", 55 | "@nx/playwright": "20.4.0", 56 | "@nx/workspace": "20.4.0", 57 | "@playwright/test": "^1.36.0", 58 | "@schematics/angular": "19.1.5", 59 | "@softarc/eslint-plugin-sheriff": "^0.15.1", 60 | "@softarc/sheriff-core": "^0.15.1", 61 | "@swc-node/register": "~1.9.1", 62 | "@swc/core": "~1.5.7", 63 | "@swc/helpers": "~0.5.11", 64 | "@types/jest": "^29.5.14", 65 | "@types/node": "18.16.9", 66 | "angular-eslint": "^19.0.2", 67 | "autoprefixer": "^10.4.19", 68 | "eslint": "^9.8.0", 69 | "eslint-config-prettier": "^9.0.0", 70 | "eslint-plugin-playwright": "^1.6.2", 71 | "eslint-plugin-unused-imports": "^4.1.4", 72 | "fake-indexeddb": "^6.0.0", 73 | "husky": "^9.0.11", 74 | "jest": "^29.7.0", 75 | "jest-environment-jsdom": "^29.7.0", 76 | "jest-preset-angular": "^14.4.2", 77 | "jsonc-eslint-parser": "^2.4.0", 78 | "lint-staged": "^15.3.0", 79 | "ng-packagr": "19.1.2", 80 | "nx": "20.4.0", 81 | "postcss": "^8.4.39", 82 | "postcss-import": "^16.1.0", 83 | "postcss-preset-env": "^9.5.15", 84 | "postcss-url": "^10.1.3", 85 | "prettier": "^2.6.2", 86 | "ts-jest": "^29.1.0", 87 | "ts-node": "10.9.1", 88 | "typescript": "5.7.3", 89 | "typescript-eslint": "^8.19.0" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /read-supported-versions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Reads the supported versions of @ngrx/signals from the package.json and saves 5 | * them into a file, which is then consumed by the integrations tests. 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const os = require('os'); 11 | 12 | // Define the path to the package.json file 13 | const packageJsonPath = path.join( 14 | __dirname, 15 | 'libs/ngrx-toolkit', 16 | 'package.json' 17 | ); 18 | // Define the path for the output file 19 | const outputPath = path.join(__dirname, 'versions.txt'); 20 | 21 | // Read the package.json file 22 | fs.readFile(packageJsonPath, 'utf8', (err, data) => { 23 | if (err) { 24 | console.error('Error reading package.json:', err); 25 | return; 26 | } 27 | 28 | // Parse the JSON content 29 | const packageJson = JSON.parse(data); 30 | 31 | // Extract dependencies 32 | const peerDependencies = packageJson.peerDependencies; 33 | if (!peerDependencies?.['@ngrx/signals']) { 34 | throw new Error('Could not find @ngrx/signals in peerDependencies'); 35 | } 36 | 37 | const versions = 38 | peerDependencies['@ngrx/signals'] 39 | .split('||') 40 | .map((version) => version.trim()) 41 | .join(os.EOL) + os.EOL; 42 | 43 | fs.writeFileSync(outputPath, versions); 44 | }); 45 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@angular-architects/ngrx-toolkit": ["libs/ngrx-toolkit/src/index.ts"], 19 | "@angular-architects/ngrx-toolkit/redux-connector": [ 20 | "libs/ngrx-toolkit/redux-connector" 21 | ] 22 | } 23 | }, 24 | "exclude": ["node_modules", "tmp"] 25 | } 26 | --------------------------------------------------------------------------------