├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .yarn └── releases │ └── yarn-4.1.0.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── angular.json ├── docs ├── introduction │ └── getting-started.md └── tutorials │ ├── quick-start.md │ └── typescript.md ├── netlify.toml ├── package.json ├── projects ├── angular-redux-auto-dispatch-demo │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ └── store │ │ │ │ ├── counter-slice.ts │ │ │ │ └── index.ts │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ └── tsconfig.app.json ├── angular-redux-injector-demo │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ ├── services │ │ │ │ └── random-number.service.ts │ │ │ ├── store │ │ │ │ ├── counter-slice.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── async-run-in-injection-context.ts │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ └── tsconfig.app.json ├── angular-redux-simple-demo │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ └── store │ │ │ │ ├── counter-slice.ts │ │ │ │ └── index.ts │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ └── tsconfig.app.json └── angular-redux │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── schematics-core │ ├── README.md │ ├── index.ts │ ├── testing │ │ ├── create-app-module.ts │ │ ├── create-package.ts │ │ ├── create-reducers.ts │ │ ├── create-workspace.ts │ │ ├── index.ts │ │ └── update.ts │ └── utility │ │ ├── ast-utils.ts │ │ ├── change.ts │ │ ├── config.ts │ │ ├── find-component.ts │ │ ├── find-module.ts │ │ ├── json-utilts.ts │ │ ├── package.ts │ │ ├── parse-name.ts │ │ ├── project.ts │ │ ├── standalone.ts │ │ ├── strings.ts │ │ ├── update.ts │ │ └── visitors.ts │ ├── schematics │ ├── collection.json │ ├── jest.config.mjs │ ├── ng-add │ │ ├── files │ │ │ └── __storePath__ │ │ │ │ ├── counter-slice.ts.template │ │ │ │ └── index.ts.template │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── schema.json │ │ └── schema.ts │ └── tsconfig.spec.json │ ├── src │ ├── lib │ │ ├── inject-dispatch.ts │ │ ├── inject-selector.ts │ │ ├── inject-store.ts │ │ ├── provide-redux.ts │ │ ├── provider.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── Subscription.ts │ │ │ └── shallowEqual.ts │ ├── public-api.ts │ └── tests │ │ ├── inject-dispatch.spec.ts │ │ ├── inject-selector-and-dispatch.spec.ts │ │ ├── inject-selector.spec.ts │ │ ├── injections.withTypes.spec.ts │ │ └── utils │ │ ├── Subscription.spec.ts │ │ └── shallowEqual.spec.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.schematics.json │ └── tsconfig.spec.json ├── tsconfig.json ├── website ├── .gitignore ├── README.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── pages │ │ ├── index.js │ │ └── styles.module.css │ └── theme │ │ └── NotFound.js └── static │ ├── css │ ├── 404.css │ ├── codeblock.css │ └── custom.css │ ├── img │ ├── external-link-square-alt-solid.svg │ ├── favicon │ │ └── favicon.ico │ ├── github-brands.svg │ ├── noun_Box_1664404.svg │ ├── noun_Certificate_1945625.svg │ ├── noun_Check_1870817.svg │ ├── noun_Rocket_1245262.svg │ ├── redux-logo-landscape.png │ ├── redux-logo-twitter.png │ ├── redux.svg │ └── redux_white.svg │ └── scripts │ ├── codeblock.js │ ├── monokaiTheme.js │ └── sidebarScroll.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [crutchcorn] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: "20.x" 14 | cache: "yarn" 15 | 16 | - name: Install packages 17 | run: yarn install 18 | 19 | - name: Run tests 20 | run: yarn test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | .cache/ 45 | .yarn/cache/ 46 | website/.yarn/ 47 | .yarnrc 48 | .yarn/* 49 | !.yarn/patches 50 | !.yarn/releases 51 | !.yarn/plugins 52 | !.yarn/sdks 53 | !.yarn/versions 54 | .pnp.* 55 | *.tgz 56 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.1.0.cjs 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | All notable changes are described on the [Releases](https://github.com/reduxjs/react-redux/releases) page. 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are open to, and grateful for, any contributions made by the community. By contributing to Angular Redux, you agree to abide by the [code of conduct](https://github.com/reduxjs/angular-redux/blob/main/CODE_OF_CONDUCT.md). 4 | 5 | Please review the [Redux Style Guide](https://redux.js.org/style-guide/style-guide) in the Redux docs to keep track of our best practices. 6 | 7 | ## Reporting Issues and Asking Questions 8 | 9 | Before opening an issue, please search the [issue tracker](https://github.com/reduxjs/angular-redux/issues) to make sure your issue hasn't already been reported. 10 | 11 | Please ask any general and implementation specific questions on [Stack Overflow with a Redux tag](http://stackoverflow.com/questions/tagged/redux?sort=votes&pageSize=50) for support. 12 | 13 | ## Development 14 | 15 | Visit the [Issue tracker](https://github.com/reduxjs/angular-redux/issues) to find a list of open issues that need attention. 16 | 17 | Fork, then clone the repo: 18 | 19 | ``` 20 | git clone https://github.com/your-username/angular-redux.git 21 | ``` 22 | 23 | This repository uses Yarn v4 to manage packages. You'll need to have Yarn v1.22 installed globally on your system first, as Yarn v4 depends on that being available first. Install dependencies with: 24 | 25 | ``` 26 | yarn install 27 | ``` 28 | 29 | ### Building 30 | 31 | Running the `build` task will create an ESM build. 32 | 33 | ``` 34 | yarn build 35 | ``` 36 | 37 | ### Testing and Linting 38 | 39 | To run the tests: 40 | 41 | ``` 42 | yarn test 43 | ``` 44 | 45 | ### New Features 46 | 47 | Please open an issue with a proposal for a new feature or refactoring before starting on the work. We don't want you to waste your efforts on a pull request that we won't want to accept. 48 | 49 | ## Submitting Changes 50 | 51 | - Open a new issue in the [Issue tracker](https://github.com/reduxjs/angular-redux/issues). 52 | - Fork the repo. 53 | - Create a new feature branch based off the `main` branch. 54 | - Make sure all tests pass and there are no linting errors. 55 | - Submit a pull request, referencing any issues it addresses. 56 | 57 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 58 | 59 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements. 60 | 61 | Thank you for contributing! 62 | 63 | # Cutting a release 64 | 65 | If you are a maintainer and want to cut a release, follow these steps: 66 | 67 | - `yarn build` 68 | - `cd dist && cd angular-redux` 69 | - `npm publish` 70 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-present Corbin Crutchley 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./projects/angular-redux/README.md -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-redux": { 7 | "projectType": "library", 8 | "root": "projects/angular-redux", 9 | "sourceRoot": "projects/angular-redux/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/angular-redux/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/angular-redux/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/angular-redux/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:jest", 29 | "options": { 30 | "exclude": ["**/schematics/ng-add/*.spec.ts"], 31 | "tsConfig": "projects/angular-redux/tsconfig.spec.json", 32 | "polyfills": ["zone.js", "zone.js/testing"] 33 | } 34 | } 35 | } 36 | }, 37 | "angular-redux-simple-demo": { 38 | "projectType": "application", 39 | "schematics": { 40 | "@schematics/angular:component": { 41 | "inlineTemplate": true, 42 | "inlineStyle": true, 43 | "skipTests": true 44 | }, 45 | "@schematics/angular:class": { 46 | "skipTests": true 47 | }, 48 | "@schematics/angular:directive": { 49 | "skipTests": true 50 | }, 51 | "@schematics/angular:guard": { 52 | "skipTests": true 53 | }, 54 | "@schematics/angular:interceptor": { 55 | "skipTests": true 56 | }, 57 | "@schematics/angular:pipe": { 58 | "skipTests": true 59 | }, 60 | "@schematics/angular:resolver": { 61 | "skipTests": true 62 | }, 63 | "@schematics/angular:service": { 64 | "skipTests": true 65 | } 66 | }, 67 | "root": "projects/angular-redux-simple-demo", 68 | "sourceRoot": "projects/angular-redux-simple-demo/src", 69 | "prefix": "app", 70 | "architect": { 71 | "build": { 72 | "builder": "@angular-devkit/build-angular:application", 73 | "options": { 74 | "outputPath": "dist/angular-redux-simple-demo", 75 | "index": "projects/angular-redux-simple-demo/src/index.html", 76 | "browser": "projects/angular-redux-simple-demo/src/main.ts", 77 | "polyfills": ["zone.js"], 78 | "tsConfig": "projects/angular-redux-simple-demo/tsconfig.app.json", 79 | "assets": [ 80 | { 81 | "glob": "**/*", 82 | "input": "projects/angular-redux-simple-demo/public" 83 | } 84 | ], 85 | "styles": ["projects/angular-redux-simple-demo/src/styles.css"], 86 | "scripts": [] 87 | }, 88 | "configurations": { 89 | "production": { 90 | "budgets": [ 91 | { 92 | "type": "initial", 93 | "maximumWarning": "500kB", 94 | "maximumError": "1MB" 95 | }, 96 | { 97 | "type": "anyComponentStyle", 98 | "maximumWarning": "2kB", 99 | "maximumError": "4kB" 100 | } 101 | ], 102 | "outputHashing": "all" 103 | }, 104 | "development": { 105 | "optimization": false, 106 | "extractLicenses": false, 107 | "sourceMap": true 108 | } 109 | }, 110 | "defaultConfiguration": "production" 111 | }, 112 | "serve": { 113 | "builder": "@angular-devkit/build-angular:dev-server", 114 | "configurations": { 115 | "production": { 116 | "buildTarget": "angular-redux-simple-demo:build:production" 117 | }, 118 | "development": { 119 | "buildTarget": "angular-redux-simple-demo:build:development" 120 | } 121 | }, 122 | "defaultConfiguration": "development" 123 | }, 124 | "extract-i18n": { 125 | "builder": "@angular-devkit/build-angular:extract-i18n" 126 | } 127 | } 128 | }, 129 | "angular-redux-injector-demo": { 130 | "projectType": "application", 131 | "schematics": {}, 132 | "root": "projects/angular-redux-injector-demo", 133 | "sourceRoot": "projects/angular-redux-injector-demo/src", 134 | "prefix": "app", 135 | "architect": { 136 | "build": { 137 | "builder": "@angular-devkit/build-angular:application", 138 | "options": { 139 | "outputPath": "dist/angular-redux-injector-demo", 140 | "index": "projects/angular-redux-injector-demo/src/index.html", 141 | "browser": "projects/angular-redux-injector-demo/src/main.ts", 142 | "polyfills": ["zone.js"], 143 | "tsConfig": "projects/angular-redux-injector-demo/tsconfig.app.json", 144 | "assets": [ 145 | { 146 | "glob": "**/*", 147 | "input": "projects/angular-redux-injector-demo/public" 148 | } 149 | ], 150 | "styles": ["projects/angular-redux-injector-demo/src/styles.css"], 151 | "scripts": [] 152 | }, 153 | "configurations": { 154 | "production": { 155 | "budgets": [ 156 | { 157 | "type": "initial", 158 | "maximumWarning": "500kB", 159 | "maximumError": "1MB" 160 | }, 161 | { 162 | "type": "anyComponentStyle", 163 | "maximumWarning": "2kB", 164 | "maximumError": "4kB" 165 | } 166 | ], 167 | "outputHashing": "all" 168 | }, 169 | "development": { 170 | "optimization": false, 171 | "extractLicenses": false, 172 | "sourceMap": true 173 | } 174 | }, 175 | "defaultConfiguration": "production" 176 | }, 177 | "serve": { 178 | "builder": "@angular-devkit/build-angular:dev-server", 179 | "configurations": { 180 | "production": { 181 | "buildTarget": "angular-redux-injector-demo:build:production" 182 | }, 183 | "development": { 184 | "buildTarget": "angular-redux-injector-demo:build:development" 185 | } 186 | }, 187 | "defaultConfiguration": "development" 188 | }, 189 | "extract-i18n": { 190 | "builder": "@angular-devkit/build-angular:extract-i18n" 191 | } 192 | } 193 | }, 194 | "angular-redux-auto-dispatch-demo": { 195 | "projectType": "application", 196 | "schematics": {}, 197 | "root": "projects/angular-redux-auto-dispatch-demo", 198 | "sourceRoot": "projects/angular-redux-auto-dispatch-demo/src", 199 | "prefix": "app", 200 | "architect": { 201 | "build": { 202 | "builder": "@angular-devkit/build-angular:application", 203 | "options": { 204 | "outputPath": "dist/angular-redux-auto-dispatch-demo", 205 | "index": "projects/angular-redux-auto-dispatch-demo/src/index.html", 206 | "browser": "projects/angular-redux-auto-dispatch-demo/src/main.ts", 207 | "polyfills": [ 208 | "zone.js" 209 | ], 210 | "tsConfig": "projects/angular-redux-auto-dispatch-demo/tsconfig.app.json", 211 | "assets": [ 212 | { 213 | "glob": "**/*", 214 | "input": "projects/angular-redux-auto-dispatch-demo/public" 215 | } 216 | ], 217 | "styles": [ 218 | "projects/angular-redux-auto-dispatch-demo/src/styles.css" 219 | ], 220 | "scripts": [] 221 | }, 222 | "configurations": { 223 | "production": { 224 | "budgets": [ 225 | { 226 | "type": "initial", 227 | "maximumWarning": "500kB", 228 | "maximumError": "1MB" 229 | }, 230 | { 231 | "type": "anyComponentStyle", 232 | "maximumWarning": "4kB", 233 | "maximumError": "8kB" 234 | } 235 | ], 236 | "outputHashing": "all" 237 | }, 238 | "development": { 239 | "optimization": false, 240 | "extractLicenses": false, 241 | "sourceMap": true 242 | } 243 | }, 244 | "defaultConfiguration": "production" 245 | }, 246 | "serve": { 247 | "builder": "@angular-devkit/build-angular:dev-server", 248 | "configurations": { 249 | "production": { 250 | "buildTarget": "angular-redux-auto-dispatch-demo:build:production" 251 | }, 252 | "development": { 253 | "buildTarget": "angular-redux-auto-dispatch-demo:build:development" 254 | } 255 | }, 256 | "defaultConfiguration": "development" 257 | }, 258 | "extract-i18n": { 259 | "builder": "@angular-devkit/build-angular:extract-i18n" 260 | } 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /docs/introduction/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting-started 3 | title: Getting Started with Angular Redux 4 | hide_title: true 5 | sidebar_label: Getting Started 6 | description: "Introduction > Getting Started: First steps with Angular Redux" 7 | --- 8 | 9 | # Getting Started with Angular Redux 10 | 11 | [Angular Redux](https://github.com/reduxjs/angular-redux) is the official [Angular](https://angular.dev/) UI bindings layer for [Redux](https://redux.js.org/). It lets your Angular components read data from a Redux store, and dispatch actions to the store to update state. 12 | 13 | ## Installation 14 | 15 | Angular Redux 2.x requires **Angular 19 or later**, in order to make use of Angular Signals. 16 | 17 | ### Installing with `ng add` 18 | 19 | You can install the Store to your project with the following `ng add` command (details here): 20 | 21 | ```sh 22 | ng add @reduxjs/angular-redux@latest 23 | ``` 24 | 25 | #### Optional `ng add` flags 26 | 27 | | flag | description | value type | default value | 28 | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------- | 29 | | `--path` | Path to the module that you wish to add the import for the `provideRedux` to. | `string` | | 30 | | `--project` | Name of the project defined in your `angular.json` to help locating the module to add the `provideRedux` to. | `string` | | 31 | | `--module` | Name of file containing the module that you wish to add the import for the `provideRedux` to. Can also include the relative path to the file. For example, `src/app/app.module.ts`. | `string` | `app` | 32 | | `--storePath` | The file path to create the state in. | `string` | `store` | 33 | 34 | This command will automate the following steps: 35 | 36 | 1. Update `package.json` > `dependencies` with Redux, Redux Toolkit, and Angular Redux 37 | 2. Run `npm install` to install those dependencies. 38 | 3. Update your `src/app/app.module.ts` > `imports` array with `provideRedux({store})` 39 | 4. If the project is using a `standalone bootstrap`, it adds `provideRedux({store})` into the application config. 40 | 41 | ### Installing with `npm` or `yarn` 42 | 43 | To use Angular Redux with your Angular app, install it as a dependency: 44 | 45 | ```bash 46 | # If you use npm: 47 | npm install @reduxjs/angular-redux 48 | 49 | # Or if you use Yarn: 50 | yarn add @reduxjs/angular-redux 51 | ``` 52 | 53 | You'll also need to [install Redux](https://redux.js.org/introduction/installation) and [set up a Redux store](https://redux.js.org/recipes/configuring-your-store/) in your app. 54 | 55 | Angular-Redux is written in TypeScript, so all types are automatically included. 56 | 57 | ## API Overview 58 | 59 | ### `provideRedux` 60 | 61 | Angular Redux includes a `provideRedux` provider factory, which makes the Redux store available to the rest of your app: 62 | 63 | ```typescript 64 | import { bootstrapApplication } from "@angular/platform-browser"; 65 | import { provideRedux } from "@reduxjs/angular-redux"; 66 | import { AppComponent } from "./app/app.component"; 67 | import { store } from "./store"; 68 | 69 | bootstrapApplication(AppComponent, { 70 | providers: [provideRedux({ store })], 71 | }); 72 | ``` 73 | 74 | ### Injectables 75 | 76 | Angular Redux provides a pair of custom Angular injectable functions that allow your Angular components to interact with the Redux store. 77 | 78 | `injectSelector` reads a value from the store state and subscribes to updates, while `injectDispatch` returns the store's `dispatch` method to let you dispatch actions. 79 | 80 | ```typescript 81 | import { Component } from "@angular/core"; 82 | import { injectSelector, injectDispatch } from "@reduxjs/angular-redux"; 83 | import { decrement, increment } from "./store/counter-slice"; 84 | import { RootState } from "./store"; 85 | 86 | @Component({ 87 | selector: "app-root", 88 | standalone: true, 89 | template: ` 90 | 91 | {{ count() }} 92 | 93 | `, 94 | }) 95 | export class AppComponent { 96 | count = injectSelector((state: RootState) => state.counter.value); 97 | dispatch = injectDispatch(); 98 | increment = increment; 99 | decrement = decrement; 100 | } 101 | ``` 102 | 103 | ## Help and Discussion 104 | 105 | The **[#redux channel](https://discord.gg/0ZcbPKXt5bZ6au5t)** of the **[Reactiflux Discord community](http://www.reactiflux.com)** is our official resource for all questions related to learning and using Redux. Reactiflux is a great place to hang out, ask questions, and learn - come join us! 106 | 107 | You can also ask questions on [Stack Overflow](https://stackoverflow.com) using the **[#redux tag](https://stackoverflow.com/questions/tagged/redux)**. 108 | -------------------------------------------------------------------------------- /docs/tutorials/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: typescript-quick-start 3 | title: TypeScript Quick Start 4 | sidebar_label: TypeScript Quick Start 5 | hide_title: true 6 | --- 7 | 8 |   9 | 10 | # Angular Redux TypeScript Quick Start 11 | 12 | :::tip What You'll Learn 13 | 14 | - How to set up and use Redux Toolkit and Angular Redux with TypeScript 15 | 16 | ::: 17 | 18 | :::info Prerequisites 19 | 20 | - Knowledge of Angular [Signals](https://angular.dev/guide/signals) 21 | - Understanding of [Redux terms and concepts](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow) 22 | - Understanding of TypeScript syntax and concepts 23 | 24 | ::: 25 | 26 | ## Introduction 27 | 28 | Welcome to the Angular Redux TypeScript Quick Start tutorial! **This tutorial will briefly show how to use TypeScript with Redux Toolkit and Angular-Redux**. 29 | 30 | This page focuses on just how to set up the TypeScript aspects. For explanations of what Redux is, how it works, and full examples of how to use Redux, see [the Redux core docs tutorials](https://redux.js.org/tutorials/index). 31 | 32 | [Angular Redux](/) is also written in TypeScript, and also includes its own type definitions. 33 | 34 | ## Project Setup 35 | 36 | ### Define Root State and Dispatch Types 37 | 38 | [Redux Toolkit's `configureStore` API](https://redux-toolkit.js.org/api/configureStore) should not need any additional typings. You will, however, want to extract the `RootState` type and the `Dispatch` type so that they can be referenced as needed. Inferring these types from the store itself means that they correctly update as you add more state slices or modify middleware settings. 39 | 40 | Since those are types, it's safe to export them directly from your store setup file such as `app/store.ts` and import them directly into other files. 41 | 42 | ```ts title="app/store.ts" 43 | import { configureStore } from "@reduxjs/toolkit"; 44 | // ... 45 | 46 | const store = configureStore({ 47 | reducer: { 48 | posts: postsReducer, 49 | comments: commentsReducer, 50 | users: usersReducer, 51 | }, 52 | }); 53 | 54 | // highlight-start 55 | // Infer the `RootState` and `AppDispatch` types from the store itself 56 | export type RootState = ReturnType; 57 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 58 | export type AppDispatch = typeof store.dispatch; 59 | // highlight-end 60 | ``` 61 | 62 | ### Define Typed Injectables 63 | 64 | While it's possible to import the `RootState` and `AppDispatch` types into each component, it's **better to create typed versions of the `injectDispatch` and `injectSelector` injectables for usage in your application**. This is important for a couple reasons: 65 | 66 | - For `injectSelector`, it saves you the need to type `(state: RootState)` every time 67 | - For `injectDispatch`, the default `Dispatch` type does not know about thunks. In order to correctly dispatch thunks, you need to use the specific customized `AppDispatch` type from the store that includes the thunk middleware types, and use that with `injectDispatch`. Adding a pre-typed `injectDispatch` injectable keeps you from forgetting to import `AppDispatch` where it's needed. 68 | 69 | Since these are actual variables, not types, it's important to define them in a separate file such as `app/injectables.ts`, not the store setup file. This allows you to import them into any component file that needs to use the injectables, and avoids potential circular import dependency issues. 70 | 71 | ```ts title="app/injectables.ts" 72 | import { injectDispatch, injectSelector } from "@reduxjs/angular-redux"; 73 | import type { RootState, AppDispatch } from "./store"; 74 | 75 | // highlight-start 76 | // Use throughout your app instead of plain `injectDispatch` and `injectSelector` 77 | export const injectAppDispatch = injectDispatch.withTypes(); 78 | export const injectAppSelector = injectSelector.withTypes(); 79 | // highlight-end 80 | ``` 81 | 82 | ## Application Usage 83 | 84 | ### Define Slice State and Action Types 85 | 86 | Each slice file should define a type for its initial state value, so that `createSlice` can correctly infer the type of `state` in each case reducer. 87 | 88 | All generated actions should be defined using the `PayloadAction` type from Redux Toolkit, which takes the type of the `action.payload` field as its generic argument. 89 | 90 | You can safely import the `RootState` type from the store file here. It's a circular import, but the TypeScript compiler can correctly handle that for types. This may be needed for use cases like writing selector functions. 91 | 92 | ```ts title="features/counter/counterSlice.ts" 93 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 94 | import type { RootState } from "../../app/store"; 95 | 96 | // highlight-start 97 | // Define a type for the slice state 98 | interface CounterState { 99 | value: number; 100 | } 101 | 102 | // Define the initial state using that type 103 | const initialState: CounterState = { 104 | value: 0, 105 | }; 106 | // highlight-end 107 | 108 | export const counterSlice = createSlice({ 109 | name: "counter", 110 | // `createSlice` will infer the state type from the `initialState` argument 111 | initialState, 112 | reducers: { 113 | increment: (state) => { 114 | state.value += 1; 115 | }, 116 | decrement: (state) => { 117 | state.value -= 1; 118 | }, 119 | // highlight-start 120 | // Use the PayloadAction type to declare the contents of `action.payload` 121 | incrementByAmount: (state, action: PayloadAction) => { 122 | // highlight-end 123 | state.value += action.payload; 124 | }, 125 | }, 126 | }); 127 | 128 | export const { increment, decrement, incrementByAmount } = counterSlice.actions; 129 | 130 | // Other code such as selectors can use the imported `RootState` type 131 | export const selectCount = (state: RootState) => state.counter.value; 132 | 133 | export default counterSlice.reducer; 134 | ``` 135 | 136 | The generated action creators will be correctly typed to accept a `payload` argument based on the `PayloadAction` type you provided for the reducer. For example, `incrementByAmount` requires a `number` as its argument. 137 | 138 | In some cases, [TypeScript may unnecessarily tighten the type of the initial state](https://github.com/reduxjs/redux-toolkit/pull/827). If that happens, you can work around it by casting the initial state using `as`, instead of declaring the type of the variable: 139 | 140 | ```ts 141 | // Workaround: cast state instead of declaring variable type 142 | const initialState = { 143 | value: 0, 144 | } as CounterState; 145 | ``` 146 | 147 | ### Use Typed Injectables in Components 148 | 149 | In component files, import the pre-typed injectables instead of the standard injectables from Angular-Redux. 150 | 151 | ```typescript title="features/counter/counter.component.ts" 152 | import { Component } from "@angular/core"; 153 | // highlight-next-line 154 | import { injectAppSelector, injectAppDispatch } from "app/injectables"; 155 | import { decrement, increment } from "./store/counter-slice"; 156 | 157 | @Component({ 158 | selector: "app-counter", 159 | standalone: true, 160 | // omit rendering logic 161 | }) 162 | export class CounterComponent { 163 | // highlight-start 164 | // The `state` arg is correctly typed as `RootState` already 165 | count = injectAppSelector((state) => state.counter.value); 166 | dispatch = injectAppDispatch(); 167 | // highlight-end 168 | increment = increment; 169 | decrement = decrement; 170 | } 171 | ``` 172 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "website" 3 | publish = "build" 4 | command = "yarn && yarn build" 5 | ignore = "git diff --quiet HEAD^ HEAD -- ../docs/ . ../netlify.toml" 6 | 7 | [build.environment] 8 | NODE_VERSION = "20" 9 | NODE_OPTIONS = "--max_old_space_size=4096" 10 | NETLIFY_USE_YARN = "true" 11 | YARN_VERSION = "1.22.10" 12 | 13 | 14 | [[plugins]] 15 | package = "netlify-plugin-cache" 16 | [plugins.inputs] 17 | paths = [ 18 | "node_modules/.cache", 19 | "website/node_modules/.cache", 20 | ".yarn/.cache" 21 | ] 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reduxjs/angular-redux-workspace", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "start:docs": "yarn workspace website run start", 8 | "build": "ng build angular-redux && tsc -p projects/angular-redux/tsconfig.schematics.json && yarn build:copy", 9 | "build:copy": "cd ./projects/angular-redux/schematics && copyfiles \"**/*.json\" ../../../dist/angular-redux/schematics && copyfiles \"**/*.template\" ../../../dist/angular-redux/schematics", 10 | "build:ng": "ng build", 11 | "build:docs": "yarn workspace website run build", 12 | "watch": "ng build --watch --configuration development", 13 | "test": "yarn test:ng && yarn test:schematics", 14 | "test:ng": "ng test", 15 | "test:schematics": "cd projects/angular-redux/schematics && jest", 16 | "prettier": "prettier --ignore-unknown .", 17 | "prettier:write": "yarn run prettier --write" 18 | }, 19 | "workspaces": { 20 | "packages": [ 21 | "projects/*", 22 | "website" 23 | ] 24 | }, 25 | "private": true, 26 | "dependencies": { 27 | "@angular/animations": "^19.0.0", 28 | "@angular/common": "^19.0.0", 29 | "@angular/compiler": "^19.0.0", 30 | "@angular/core": "^19.0.0", 31 | "@angular/forms": "^19.0.0", 32 | "@angular/platform-browser": "^19.0.0", 33 | "@angular/platform-browser-dynamic": "^19.0.0", 34 | "@angular/router": "^19.0.0", 35 | "@reduxjs/toolkit": "^2.2.7", 36 | "redux": "^5.0.1", 37 | "rxjs": "~7.8.0", 38 | "tslib": "^2.3.0", 39 | "zone.js": "~0.15.0" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "^19.0.1", 43 | "@angular/cli": "^19.0.1", 44 | "@angular/compiler-cli": "^19.0.0", 45 | "@testing-library/angular": "^17.3.1", 46 | "@testing-library/dom": "^10.0.0", 47 | "@testing-library/jest-dom": "^6.4.8", 48 | "@testing-library/user-event": "^14.5.2", 49 | "@types/copyfiles": "^2", 50 | "@types/jasmine": "~5.1.0", 51 | "@types/jest": "^29.5.12", 52 | "@types/node": "^22.5.4", 53 | "copyfiles": "^2.4.1", 54 | "jasmine-core": "~5.2.0", 55 | "jest": "^29.7.0", 56 | "jest-environment-jsdom": "^29.7.0", 57 | "ng-packagr": "^19.0.1", 58 | "prettier": "^3.3.3", 59 | "ts-jest": "^29.2.5", 60 | "typescript": "~5.5.2" 61 | }, 62 | "packageManager": "yarn@4.1.0" 63 | } 64 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/angular-redux/c934ad5a6f3d9d7f973db31a4bf0ed2c909e7166/projects/angular-redux-auto-dispatch-demo/public/favicon.ico -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect } from '@angular/core'; 2 | import { injectSelector, injectDispatch } from '@reduxjs/angular-redux'; 3 | import { decrement, increment } from './store/counter-slice'; 4 | import { RootState } from './store'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | standalone: true, 9 | template: ` 10 | 11 | {{ count() }} 12 | 13 | `, 14 | }) 15 | export class AppComponent { 16 | count = injectSelector((state: RootState) => state.counter.value); 17 | dispatch = injectDispatch(); 18 | increment = increment; 19 | decrement = decrement; 20 | 21 | _auto_increment = effect(() => { 22 | this.dispatch(increment()); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRedux } from '@reduxjs/angular-redux'; 3 | import { store } from './store'; 4 | 5 | export const appConfig: ApplicationConfig = { 6 | providers: [ 7 | provideZoneChangeDetection({ eventCoalescing: true }), 8 | provideRedux({ store }), 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/src/app/store/counter-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import type { PayloadAction } from '@reduxjs/toolkit'; 3 | 4 | export interface CounterState { 5 | value: number; 6 | } 7 | 8 | const initialState: CounterState = { 9 | value: 0, 10 | }; 11 | 12 | export const counterSlice = createSlice({ 13 | name: 'counter', 14 | initialState, 15 | reducers: { 16 | increment: (state) => { 17 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 18 | // doesn't actually mutate the state because it uses the Immer library, 19 | // which detects changes to a "draft state" and produces a brand new 20 | // immutable state based off those changes 21 | state.value += 1; 22 | }, 23 | decrement: (state) => { 24 | state.value -= 1; 25 | }, 26 | incrementByAmount: (state, action: PayloadAction) => { 27 | state.value += action.payload; 28 | }, 29 | }, 30 | }); 31 | 32 | // Action creators are generated for each case reducer function 33 | export const { increment, decrement, incrementByAmount } = counterSlice.actions; 34 | 35 | export default counterSlice.reducer; 36 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import counterReducer from './counter-slice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | counter: counterReducer, 7 | }, 8 | }); 9 | 10 | // Infer the `RootState` and `AppDispatch` types from the store itself 11 | export type RootState = ReturnType; 12 | // Inferred type: {counter: CounterState} 13 | export type AppDispatch = typeof store.dispatch; 14 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularReduxDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-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 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-redux-auto-dispatch-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/angular-redux/c934ad5a6f3d9d7f973db31a4bf0ed2c909e7166/projects/angular-redux-injector-demo/public/favicon.ico -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EnvironmentInjector, inject } from '@angular/core'; 2 | import { injectSelector, injectDispatch } from '@reduxjs/angular-redux'; 3 | import { incrementByRandomNumber } from './store/counter-slice'; 4 | import { AppDispatch, RootState } from './store'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | standalone: true, 9 | template: ` 10 | 13 |

{{ count() }}

14 | @if (isLoading()) { 15 |

Loading...

16 | } 17 | `, 18 | }) 19 | export class AppComponent { 20 | injector = inject(EnvironmentInjector); 21 | count = injectSelector((state: RootState) => state.counter.value); 22 | isLoading = injectSelector((state: RootState) => state.counter.isLoading); 23 | dispatch = injectDispatch(); 24 | incrementByRandomNumber = () => { 25 | this.dispatch(incrementByRandomNumber({ injector: this.injector })); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRedux } from '@reduxjs/angular-redux'; 3 | import { store } from './store'; 4 | 5 | export const appConfig: ApplicationConfig = { 6 | providers: [ 7 | provideZoneChangeDetection({ eventCoalescing: true }), 8 | provideRedux({ store }), 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/app/services/random-number.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class RandomNumberService { 5 | getRandomNumber() { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(Math.floor(Math.random() * 100)); 9 | }, 1000); 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/app/store/counter-slice.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { inject } from '@angular/core'; 3 | import { RandomNumberService } from '../services/random-number.service'; 4 | import { 5 | asyncRunInInjectionContext, 6 | RunInInjectionContextProps, 7 | } from '../utils/async-run-in-injection-context'; 8 | 9 | export const incrementByRandomNumber = createAsyncThunk( 10 | 'counter/incrementByAmountFromService', 11 | (arg: RunInInjectionContextProps<{}>, _thunkAPI) => { 12 | return asyncRunInInjectionContext(arg.injector, async () => { 13 | const service = inject(RandomNumberService); 14 | const newCount = await service.getRandomNumber(); 15 | return newCount; 16 | }); 17 | }, 18 | ); 19 | 20 | export const counterSlice = createSlice({ 21 | name: 'counter', 22 | initialState: { 23 | value: 0, 24 | isLoading: false, 25 | }, 26 | reducers: {}, 27 | extraReducers: (builder) => { 28 | builder.addCase(incrementByRandomNumber.fulfilled, (state, action) => { 29 | state.isLoading = false; 30 | state.value += action.payload; 31 | }); 32 | builder.addCase(incrementByRandomNumber.pending, (state) => { 33 | state.isLoading = true; 34 | }); 35 | }, 36 | }); 37 | 38 | export default counterSlice.reducer; 39 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import counterReducer from './counter-slice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | counter: counterReducer, 7 | }, 8 | }); 9 | 10 | // Infer the `RootState` and `AppDispatch` types from the store itself 11 | export type RootState = ReturnType; 12 | // Inferred type: {counter: CounterState} 13 | export type AppDispatch = typeof store.dispatch; 14 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/app/utils/async-run-in-injection-context.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentInjector, runInInjectionContext } from '@angular/core'; 2 | 3 | export const asyncRunInInjectionContext = ( 4 | injector: EnvironmentInjector, 5 | fn: () => Promise, 6 | ) => { 7 | return new Promise((resolve, reject) => { 8 | runInInjectionContext(injector, () => { 9 | fn() 10 | .then((value) => { 11 | resolve(value); 12 | }) 13 | .catch((error) => { 14 | reject(error); 15 | }); 16 | }); 17 | }); 18 | }; 19 | 20 | export type RunInInjectionContextProps = T & { 21 | injector: EnvironmentInjector; 22 | }; 23 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularReduxInjectorDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-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 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-redux-injector-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": ["src/main.ts"], 10 | "include": ["src/**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/angular-redux/c934ad5a6f3d9d7f973db31a4bf0ed2c909e7166/projects/angular-redux-simple-demo/public/favicon.ico -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { injectSelector, injectDispatch } from '@reduxjs/angular-redux'; 3 | import { decrement, increment } from './store/counter-slice'; 4 | import { RootState } from './store'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | standalone: true, 9 | template: ` 10 | 11 | {{ count() }} 12 | 13 | `, 14 | }) 15 | export class AppComponent { 16 | count = injectSelector((state: RootState) => state.counter.value); 17 | dispatch = injectDispatch(); 18 | increment = increment; 19 | decrement = decrement; 20 | } 21 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRedux } from '@reduxjs/angular-redux'; 3 | import { store } from './store'; 4 | 5 | export const appConfig: ApplicationConfig = { 6 | providers: [ 7 | provideZoneChangeDetection({ eventCoalescing: true }), 8 | provideRedux({ store }), 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/src/app/store/counter-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import type { PayloadAction } from '@reduxjs/toolkit'; 3 | 4 | export interface CounterState { 5 | value: number; 6 | } 7 | 8 | const initialState: CounterState = { 9 | value: 0, 10 | }; 11 | 12 | export const counterSlice = createSlice({ 13 | name: 'counter', 14 | initialState, 15 | reducers: { 16 | increment: (state) => { 17 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 18 | // doesn't actually mutate the state because it uses the Immer library, 19 | // which detects changes to a "draft state" and produces a brand new 20 | // immutable state based off those changes 21 | state.value += 1; 22 | }, 23 | decrement: (state) => { 24 | state.value -= 1; 25 | }, 26 | incrementByAmount: (state, action: PayloadAction) => { 27 | state.value += action.payload; 28 | }, 29 | }, 30 | }); 31 | 32 | // Action creators are generated for each case reducer function 33 | export const { increment, decrement, incrementByAmount } = counterSlice.actions; 34 | 35 | export default counterSlice.reducer; 36 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import counterReducer from './counter-slice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | counter: counterReducer, 7 | }, 8 | }); 9 | 10 | // Infer the `RootState` and `AppDispatch` types from the store itself 11 | export type RootState = ReturnType; 12 | // Inferred type: {counter: CounterState} 13 | export type AppDispatch = typeof store.dispatch; 14 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularReduxDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-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 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-redux-simple-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": ["src/main.ts"], 10 | "include": ["src/**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /projects/angular-redux/README.md: -------------------------------------------------------------------------------- 1 | # Angular Redux 2 | 3 | Official Angular bindings for [Redux](https://github.com/reduxjs/redux). 4 | Performant and flexible. 5 | 6 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/reduxjs/angular-redux/test.yml?style=flat-square) [![npm version](https://img.shields.io/npm/v/@reduxjs/angular-redux.svg?style=flat-square)](https://www.npmjs.com/package/@reduxjs/angular-redux) 7 | [![npm downloads](https://img.shields.io/npm/dm/@reduxjs/angular-redux.svg?style=flat-square)](https://www.npmjs.com/package/@reduxjs/angular-redux) 8 | [![#redux channel on Discord](https://img.shields.io/badge/discord-redux@reactiflux-61DAFB.svg?style=flat-square)](http://www.reactiflux.com) 9 | 10 | ## Installation 11 | 12 | Angular Redux requires **Angular 19 or later**. 13 | 14 | ### Installing with `ng add` 15 | 16 | You can install the Store to your project with the following `ng add` command (details here): 17 | 18 | ```sh 19 | ng add @reduxjs/angular-redux@latest 20 | ``` 21 | 22 | #### Optional `ng add` flags 23 | 24 | | flag | description | value type | default value | 25 | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------- | 26 | | `--path` | Path to the module that you wish to add the import for the StoreModule to. | `string` | 27 | | `--project` | Name of the project defined in your `angular.json` to help locating the module to add the `provideRedux` to. | `string` | 28 | | `--module` | Name of file containing the module that you wish to add the import for the `provideRedux` to. Can also include the relative path to the file. For example, `src/app/app.module.ts`. | `string` | `app` | 29 | | `--storePath` | The file path to create the state in. | `string` | `store` | 30 | 31 | This command will automate the following steps: 32 | 33 | 1. Update `package.json` > `dependencies` with Redux, Redux Toolkit, and Angular Redux 34 | 2. Run `npm install` to install those dependencies. 35 | 3. Update your `src/app/app.module.ts` > `imports` array with `provideRedux({store})` 36 | 4. If the project is using a `standalone bootstrap`, it adds `provideRedux({store})` into the application config. 37 | 38 | ## Installing with `npm` or `yarn` 39 | 40 | To use React Redux with your Angular app, install it as a dependency: 41 | 42 | ```bash 43 | # If you use npm: 44 | npm install @reduxjs/angular-redux 45 | 46 | # Or if you use Yarn: 47 | yarn add @reduxjs/angular-redux 48 | ``` 49 | 50 | You'll also need to [install Redux](https://redux.js.org/introduction/installation) and [set up a Redux store](https://redux.js.org/recipes/configuring-your-store/) in your app. 51 | 52 | ## Documentation 53 | 54 | The React Redux docs are published at **https://angular-redux.js.org** . 55 | 56 | ## License 57 | 58 | [MIT](LICENSE.md) 59 | -------------------------------------------------------------------------------- /projects/angular-redux/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-redux", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reduxjs/angular-redux", 3 | "version": "2.0.1", 4 | "keywords": [ 5 | "angular", 6 | "redux" 7 | ], 8 | "license": "MIT", 9 | "author": "Corbin Cruthcley (https://github.com/crutchcorn)", 10 | "homepage": "https://github.com/reduxjs/angular-redux", 11 | "repository": "github:reduxjs/angular-redux", 12 | "bugs": "https://github.com/reduxjs/angular-redux/issues", 13 | "peerDependencies": { 14 | "@angular/common": ">=19.0.0", 15 | "@angular/core": ">=19.0.0", 16 | "@reduxjs/toolkit": "^2.2.7", 17 | "redux": "^5.0.0" 18 | }, 19 | "peerDependenciesMeta": { 20 | "@reduxjs/toolkit": { 21 | "optional": true 22 | }, 23 | "redux": { 24 | "optional": true 25 | } 26 | }, 27 | "schematics": "./schematics/collection.json", 28 | "dependencies": { 29 | "tslib": "^2.3.0" 30 | }, 31 | "sideEffects": false 32 | } 33 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/README.md: -------------------------------------------------------------------------------- 1 | This code is originally from NgRx: 2 | 3 | https://github.com/ngrx/platform/tree/main/modules/schematics-core 4 | https://github.com/ngrx/platform/tree/main/modules/store/schematics-core 5 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dasherize, 3 | decamelize, 4 | camelize, 5 | classify, 6 | underscore, 7 | group, 8 | capitalize, 9 | featurePath, 10 | pluralize, 11 | } from './utility/strings'; 12 | 13 | export { 14 | findNodes, 15 | getSourceNodes, 16 | getDecoratorMetadata, 17 | getContentOfKeyLiteral, 18 | insertAfterLastOccurrence, 19 | insertImport, 20 | addBootstrapToModule, 21 | addDeclarationToModule, 22 | addExportToModule, 23 | addImportToModule, 24 | addProviderToComponent, 25 | addProviderToModule, 26 | replaceImport, 27 | containsProperty, 28 | } from './utility/ast-utils'; 29 | 30 | export { 31 | NoopChange, 32 | InsertChange, 33 | RemoveChange, 34 | ReplaceChange, 35 | createReplaceChange, 36 | createChangeRecorder, 37 | commitChanges, 38 | } from './utility/change'; 39 | export type { Host, Change } from './utility/change'; 40 | 41 | export { getWorkspace, getWorkspacePath } from './utility/config'; 42 | export type { AppConfig } from './utility/config'; 43 | 44 | export { findComponentFromOptions } from './utility/find-component'; 45 | 46 | export { 47 | findModule, 48 | findModuleFromOptions, 49 | buildRelativePath, 50 | } from './utility/find-module'; 51 | export type { ModuleOptions } from './utility/find-module'; 52 | 53 | export { findPropertyInAstObject } from './utility/json-utilts'; 54 | 55 | export { getProjectPath, getProject, isLib } from './utility/project'; 56 | 57 | export const stringUtils = { 58 | dasherize, 59 | decamelize, 60 | camelize, 61 | classify, 62 | underscore, 63 | group, 64 | capitalize, 65 | featurePath, 66 | pluralize, 67 | }; 68 | 69 | export { parseName } from './utility/parse-name'; 70 | 71 | export { addPackageToPackageJson } from './utility/package'; 72 | 73 | export { 74 | visitTSSourceFiles, 75 | visitNgModuleImports, 76 | visitNgModuleExports, 77 | visitComponents, 78 | visitDecorator, 79 | visitNgModules, 80 | visitTemplates, 81 | } from './utility/visitors'; 82 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/testing/create-app-module.ts: -------------------------------------------------------------------------------- 1 | import { UnitTestTree } from '@angular-devkit/schematics/testing'; 2 | 3 | export function createAppModule( 4 | tree: UnitTestTree, 5 | path?: string, 6 | ): UnitTestTree { 7 | tree.create( 8 | path || '/src/app/app.module.ts', 9 | ` 10 | import { BrowserModule } from '@angular/platform-browser'; 11 | import { NgModule } from '@angular/core'; 12 | import { AppComponent } from './app.component'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent 17 | ], 18 | imports: [ 19 | BrowserModule 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent] 23 | }) 24 | export class AppModule { } 25 | `, 26 | ); 27 | 28 | return tree; 29 | } 30 | 31 | export function createAppModuleWithEffects( 32 | tree: UnitTestTree, 33 | path: string, 34 | effects?: string, 35 | ): UnitTestTree { 36 | tree.create( 37 | path || '/src/app/app.module.ts', 38 | ` 39 | import { BrowserModule } from '@angular/platform-browser'; 40 | import { NgModule } from '@angular/core'; 41 | import { AppComponent } from './app.component'; 42 | import { EffectsModule } from '@ngrx/effects'; 43 | 44 | @NgModule({ 45 | declarations: [ 46 | AppComponent 47 | ], 48 | imports: [ 49 | BrowserModule, 50 | ${effects} 51 | ], 52 | providers: [], 53 | bootstrap: [AppComponent] 54 | }) 55 | export class AppModule { } 56 | `, 57 | ); 58 | 59 | return tree; 60 | } 61 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/testing/create-package.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { 3 | UnitTestTree, 4 | SchematicTestRunner, 5 | } from '@angular-devkit/schematics/testing'; 6 | 7 | export const packagePath = '/package.json'; 8 | 9 | export function createPackageJson( 10 | prefix: string, 11 | pkg: string, 12 | tree: UnitTestTree, 13 | version = '5.2.0', 14 | packagePath = '/package.json', 15 | ) { 16 | tree.create( 17 | packagePath, 18 | `{ 19 | "dependencies": { 20 | "@ngrx/${pkg}": "${prefix}${version}" 21 | } 22 | }`, 23 | ); 24 | 25 | return tree; 26 | } 27 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/testing/create-reducers.ts: -------------------------------------------------------------------------------- 1 | import { UnitTestTree } from '@angular-devkit/schematics/testing'; 2 | 3 | export function createReducers( 4 | tree: UnitTestTree, 5 | path?: string, 6 | project = 'bar', 7 | ) { 8 | tree.create( 9 | path || `/projects/${project}/src/app/reducers/index.ts`, 10 | ` 11 | import { isDevMode } from '@angular/core'; 12 | import { 13 | ActionReducer, 14 | ActionReducerMap, 15 | createFeatureSelector, 16 | createSelector, 17 | MetaReducer 18 | } from '@ngrx/${'store'}'; 19 | 20 | export interface State { 21 | 22 | } 23 | 24 | export const reducers: ActionReducerMap = { 25 | 26 | }; 27 | 28 | 29 | export const metaReducers: MetaReducer[] = isDevMode() ? [] : []; 30 | `, 31 | ); 32 | 33 | return tree; 34 | } 35 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/testing/create-workspace.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchematicTestRunner, 3 | UnitTestTree, 4 | } from '@angular-devkit/schematics/testing'; 5 | 6 | export const defaultWorkspaceOptions = { 7 | name: 'workspace', 8 | newProjectRoot: 'projects', 9 | version: '6.0.0', 10 | }; 11 | 12 | export const defaultAppOptions = { 13 | name: 'bar', 14 | inlineStyle: false, 15 | inlineTemplate: false, 16 | viewEncapsulation: 'Emulated', 17 | routing: false, 18 | style: 'css', 19 | skipTests: false, 20 | standalone: false, 21 | }; 22 | 23 | const defaultLibOptions = { 24 | name: 'baz', 25 | }; 26 | 27 | export function getTestProjectPath( 28 | workspaceOptions: any = defaultWorkspaceOptions, 29 | appOptions: any = defaultAppOptions, 30 | ) { 31 | return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; 32 | } 33 | 34 | export async function createWorkspace( 35 | schematicRunner: SchematicTestRunner, 36 | appTree: UnitTestTree, 37 | workspaceOptions = defaultWorkspaceOptions, 38 | appOptions = defaultAppOptions, 39 | libOptions = defaultLibOptions, 40 | ) { 41 | appTree = await schematicRunner.runExternalSchematic( 42 | '@schematics/angular', 43 | 'workspace', 44 | workspaceOptions, 45 | ); 46 | 47 | appTree = await schematicRunner.runExternalSchematic( 48 | '@schematics/angular', 49 | 'application', 50 | appOptions, 51 | appTree, 52 | ); 53 | 54 | appTree = await schematicRunner.runExternalSchematic( 55 | '@schematics/angular', 56 | 'application', 57 | { ...appOptions, name: 'bar-standalone', standalone: true }, 58 | appTree, 59 | ); 60 | 61 | appTree = await schematicRunner.runExternalSchematic( 62 | '@schematics/angular', 63 | 'library', 64 | libOptions, 65 | appTree, 66 | ); 67 | 68 | return appTree; 69 | } 70 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-app-module'; 2 | export * from './create-reducers'; 3 | export * from './create-workspace'; 4 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/testing/update.ts: -------------------------------------------------------------------------------- 1 | export const upgradeVersion = '6.0.0'; 2 | export const versionPrefixes = ['~', '^', '']; 3 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/change.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { Tree, UpdateRecorder } from '@angular-devkit/schematics'; 3 | import { Path } from '@angular-devkit/core'; 4 | 5 | /* istanbul ignore file */ 6 | /** 7 | * @license 8 | * Copyright Google Inc. All Rights Reserved. 9 | * 10 | * Use of this source code is governed by an MIT-style license that can be 11 | * found in the LICENSE file at https://angular.io/license 12 | */ 13 | export interface Host { 14 | write(path: string, content: string): Promise; 15 | read(path: string): Promise; 16 | } 17 | 18 | export interface Change { 19 | apply(host: Host): Promise; 20 | 21 | // The file this change should be applied to. Some changes might not apply to 22 | // a file (maybe the config). 23 | readonly path: string | null; 24 | 25 | // The order this change should be applied. Normally the position inside the file. 26 | // Changes are applied from the bottom of a file to the top. 27 | readonly order: number; 28 | 29 | // The description of this change. This will be outputted in a dry or verbose run. 30 | readonly description: string; 31 | } 32 | 33 | /** 34 | * An operation that does nothing. 35 | */ 36 | export class NoopChange implements Change { 37 | description = 'No operation.'; 38 | order = Infinity; 39 | path = null; 40 | apply() { 41 | return Promise.resolve(); 42 | } 43 | } 44 | 45 | /** 46 | * Will add text to the source code. 47 | */ 48 | export class InsertChange implements Change { 49 | order: number; 50 | description: string; 51 | 52 | constructor( 53 | public path: string, 54 | public pos: number, 55 | public toAdd: string, 56 | ) { 57 | if (pos < 0) { 58 | throw new Error('Negative positions are invalid'); 59 | } 60 | this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; 61 | this.order = pos; 62 | } 63 | 64 | /** 65 | * This method does not insert spaces if there is none in the original string. 66 | */ 67 | apply(host: Host) { 68 | return host.read(this.path).then((content) => { 69 | const prefix = content.substring(0, this.pos); 70 | const suffix = content.substring(this.pos); 71 | 72 | return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); 73 | }); 74 | } 75 | } 76 | 77 | /** 78 | * Will remove text from the source code. 79 | */ 80 | export class RemoveChange implements Change { 81 | order: number; 82 | description: string; 83 | 84 | constructor( 85 | public path: string, 86 | public pos: number, 87 | public end: number, 88 | ) { 89 | if (pos < 0 || end < 0) { 90 | throw new Error('Negative positions are invalid'); 91 | } 92 | this.description = `Removed text in position ${pos} to ${end} of ${path}`; 93 | this.order = pos; 94 | } 95 | 96 | apply(host: Host): Promise { 97 | return host.read(this.path).then((content) => { 98 | const prefix = content.substring(0, this.pos); 99 | const suffix = content.substring(this.end); 100 | 101 | // TODO: throw error if toRemove doesn't match removed string. 102 | return host.write(this.path, `${prefix}${suffix}`); 103 | }); 104 | } 105 | } 106 | 107 | /** 108 | * Will replace text from the source code. 109 | */ 110 | export class ReplaceChange implements Change { 111 | order: number; 112 | description: string; 113 | 114 | constructor( 115 | public path: string, 116 | public pos: number, 117 | public oldText: string, 118 | public newText: string, 119 | ) { 120 | if (pos < 0) { 121 | throw new Error('Negative positions are invalid'); 122 | } 123 | this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; 124 | this.order = pos; 125 | } 126 | 127 | apply(host: Host): Promise { 128 | return host.read(this.path).then((content) => { 129 | const prefix = content.substring(0, this.pos); 130 | const suffix = content.substring(this.pos + this.oldText.length); 131 | const text = content.substring(this.pos, this.pos + this.oldText.length); 132 | 133 | if (text !== this.oldText) { 134 | return Promise.reject( 135 | new Error(`Invalid replace: "${text}" != "${this.oldText}".`), 136 | ); 137 | } 138 | 139 | // TODO: throw error if oldText doesn't match removed string. 140 | return host.write(this.path, `${prefix}${this.newText}${suffix}`); 141 | }); 142 | } 143 | } 144 | 145 | export function createReplaceChange( 146 | sourceFile: ts.SourceFile, 147 | node: ts.Node, 148 | oldText: string, 149 | newText: string, 150 | ): ReplaceChange { 151 | return new ReplaceChange( 152 | sourceFile.fileName, 153 | node.getStart(sourceFile), 154 | oldText, 155 | newText, 156 | ); 157 | } 158 | 159 | export function createRemoveChange( 160 | sourceFile: ts.SourceFile, 161 | node: ts.Node, 162 | from = node.getStart(sourceFile), 163 | to = node.getEnd(), 164 | ): RemoveChange { 165 | return new RemoveChange(sourceFile.fileName, from, to); 166 | } 167 | 168 | export function createChangeRecorder( 169 | tree: Tree, 170 | path: string, 171 | changes: Change[], 172 | ): UpdateRecorder { 173 | const recorder = tree.beginUpdate(path); 174 | for (const change of changes) { 175 | if (change instanceof InsertChange) { 176 | recorder.insertLeft(change.pos, change.toAdd); 177 | } else if (change instanceof RemoveChange) { 178 | recorder.remove(change.pos, change.end - change.pos); 179 | } else if (change instanceof ReplaceChange) { 180 | recorder.remove(change.pos, change.oldText.length); 181 | recorder.insertLeft(change.pos, change.newText); 182 | } 183 | } 184 | return recorder; 185 | } 186 | 187 | export function commitChanges(tree: Tree, path: string, changes: Change[]) { 188 | if (changes.length === 0) { 189 | return false; 190 | } 191 | 192 | const recorder = createChangeRecorder(tree, path, changes); 193 | tree.commitUpdate(recorder); 194 | return true; 195 | } 196 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/config.ts: -------------------------------------------------------------------------------- 1 | import { SchematicsException, Tree } from '@angular-devkit/schematics'; 2 | 3 | // The interfaces below are generated from the Angular CLI configuration schema 4 | // https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json 5 | export interface AppConfig { 6 | /** 7 | * Name of the app. 8 | */ 9 | name?: string; 10 | /** 11 | * Directory where app files are placed. 12 | */ 13 | appRoot?: string; 14 | /** 15 | * The root directory of the app. 16 | */ 17 | root?: string; 18 | /** 19 | * The output directory for build results. 20 | */ 21 | outDir?: string; 22 | /** 23 | * List of application assets. 24 | */ 25 | assets?: ( 26 | | string 27 | | { 28 | /** 29 | * The pattern to match. 30 | */ 31 | glob?: string; 32 | /** 33 | * The dir to search within. 34 | */ 35 | input?: string; 36 | /** 37 | * The output path (relative to the outDir). 38 | */ 39 | output?: string; 40 | } 41 | )[]; 42 | /** 43 | * URL where files will be deployed. 44 | */ 45 | deployUrl?: string; 46 | /** 47 | * Base url for the application being built. 48 | */ 49 | baseHref?: string; 50 | /** 51 | * The runtime platform of the app. 52 | */ 53 | platform?: 'browser' | 'server'; 54 | /** 55 | * The name of the start HTML file. 56 | */ 57 | index?: string; 58 | /** 59 | * The name of the main entry-point file. 60 | */ 61 | main?: string; 62 | /** 63 | * The name of the polyfills file. 64 | */ 65 | polyfills?: string; 66 | /** 67 | * The name of the test entry-point file. 68 | */ 69 | test?: string; 70 | /** 71 | * The name of the TypeScript configuration file. 72 | */ 73 | tsconfig?: string; 74 | /** 75 | * The name of the TypeScript configuration file for unit tests. 76 | */ 77 | testTsconfig?: string; 78 | /** 79 | * The prefix to apply to generated selectors. 80 | */ 81 | prefix?: string; 82 | /** 83 | * Experimental support for a service worker from @angular/service-worker. 84 | */ 85 | serviceWorker?: boolean; 86 | /** 87 | * Global styles to be included in the build. 88 | */ 89 | styles?: ( 90 | | string 91 | | { 92 | input?: string; 93 | [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any 94 | } 95 | )[]; 96 | /** 97 | * Options to pass to style preprocessors 98 | */ 99 | stylePreprocessorOptions?: { 100 | /** 101 | * Paths to include. Paths will be resolved to project root. 102 | */ 103 | includePaths?: string[]; 104 | }; 105 | /** 106 | * Global scripts to be included in the build. 107 | */ 108 | scripts?: ( 109 | | string 110 | | { 111 | input: string; 112 | [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any 113 | } 114 | )[]; 115 | /** 116 | * Source file for environment config. 117 | */ 118 | environmentSource?: string; 119 | /** 120 | * Name and corresponding file for environment config. 121 | */ 122 | environments?: { 123 | [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any 124 | }; 125 | appShell?: { 126 | app: string; 127 | route: string; 128 | }; 129 | } 130 | 131 | export function getWorkspacePath(host: Tree): string { 132 | const possibleFiles = ['/angular.json', '/.angular.json', '/workspace.json']; 133 | const path = possibleFiles.filter((path) => host.exists(path))[0]; 134 | 135 | return path; 136 | } 137 | 138 | export function getWorkspace(host: Tree) { 139 | const path = getWorkspacePath(host); 140 | const configBuffer = host.read(path); 141 | if (configBuffer === null) { 142 | throw new SchematicsException(`Could not find (${path})`); 143 | } 144 | const config = configBuffer.toString(); 145 | 146 | return JSON.parse(config); 147 | } 148 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/find-component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | Path, 10 | join, 11 | normalize, 12 | relative, 13 | strings, 14 | basename, 15 | extname, 16 | dirname, 17 | } from '@angular-devkit/core'; 18 | import { DirEntry, Tree } from '@angular-devkit/schematics'; 19 | 20 | export interface ComponentOptions { 21 | component?: string; 22 | name: string; 23 | flat?: boolean; 24 | path?: string; 25 | skipImport?: boolean; 26 | } 27 | 28 | /** 29 | * Find the component referred by a set of options passed to the schematics. 30 | */ 31 | export function findComponentFromOptions( 32 | host: Tree, 33 | options: ComponentOptions, 34 | ): Path | undefined { 35 | if (options.hasOwnProperty('skipImport') && options.skipImport) { 36 | return undefined; 37 | } 38 | 39 | if (!options.component) { 40 | const pathToCheck = 41 | (options.path || '') + 42 | (options.flat ? '' : '/' + strings.dasherize(options.name)); 43 | 44 | return normalize(findComponent(host, pathToCheck)); 45 | } else { 46 | const componentPath = normalize( 47 | '/' + options.path + '/' + options.component, 48 | ); 49 | const componentBaseName = normalize(componentPath).split('/').pop(); 50 | 51 | if (host.exists(componentPath)) { 52 | return normalize(componentPath); 53 | } else if (host.exists(componentPath + '.ts')) { 54 | return normalize(componentPath + '.ts'); 55 | } else if (host.exists(componentPath + '.component.ts')) { 56 | return normalize(componentPath + '.component.ts'); 57 | } else if ( 58 | host.exists(componentPath + '/' + componentBaseName + '.component.ts') 59 | ) { 60 | return normalize( 61 | componentPath + '/' + componentBaseName + '.component.ts', 62 | ); 63 | } else { 64 | throw new Error( 65 | `Specified component path ${componentPath} does not exist`, 66 | ); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Function to find the "closest" component to a generated file's path. 73 | */ 74 | export function findComponent(host: Tree, generateDir: string): Path { 75 | let dir: DirEntry | null = host.getDir('/' + generateDir); 76 | 77 | const componentRe = /\.component\.ts$/; 78 | 79 | while (dir) { 80 | const matches = dir.subfiles.filter((p) => componentRe.test(p)); 81 | 82 | if (matches.length == 1) { 83 | return join(dir.path, matches[0]); 84 | } else if (matches.length > 1) { 85 | throw new Error( 86 | 'More than one component matches. Use skip-import option to skip importing ' + 87 | 'the component store into the closest component.', 88 | ); 89 | } 90 | 91 | dir = dir.parent; 92 | } 93 | 94 | throw new Error( 95 | 'Could not find an Component. Use the skip-import ' + 96 | 'option to skip importing in Component.', 97 | ); 98 | } 99 | 100 | /** 101 | * Build a relative path from one file path to another file path. 102 | */ 103 | export function buildRelativePath(from: string, to: string): string { 104 | const { 105 | path: fromPath, 106 | filename: fromFileName, 107 | directory: fromDirectory, 108 | } = parsePath(from); 109 | const { 110 | path: toPath, 111 | filename: toFileName, 112 | directory: toDirectory, 113 | } = parsePath(to); 114 | const relativePath = relative(fromDirectory, toDirectory); 115 | const fixedRelativePath = relativePath.startsWith('.') 116 | ? relativePath 117 | : `./${relativePath}`; 118 | 119 | return !toFileName || toFileName === 'index.ts' 120 | ? fixedRelativePath 121 | : `${ 122 | fixedRelativePath.endsWith('/') 123 | ? fixedRelativePath 124 | : fixedRelativePath + '/' 125 | }${convertToTypeScriptFileName(toFileName)}`; 126 | } 127 | 128 | function parsePath(path: string) { 129 | const pathNormalized = normalize(path) as Path; 130 | const filename = extname(pathNormalized) ? basename(pathNormalized) : ''; 131 | const directory = filename ? dirname(pathNormalized) : pathNormalized; 132 | return { 133 | path: pathNormalized, 134 | filename, 135 | directory, 136 | }; 137 | } 138 | /** 139 | * Strips the typescript extension and clears index filenames 140 | * foo.ts -> foo 141 | * index.ts -> empty 142 | */ 143 | function convertToTypeScriptFileName(filename: string | undefined) { 144 | return filename ? filename.replace(/(\.ts)|(index\.ts)$/, '') : ''; 145 | } 146 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/find-module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | Path, 10 | join, 11 | normalize, 12 | relative, 13 | strings, 14 | basename, 15 | extname, 16 | dirname, 17 | } from '@angular-devkit/core'; 18 | import { DirEntry, Tree } from '@angular-devkit/schematics'; 19 | 20 | export interface ModuleOptions { 21 | module?: string; 22 | name: string; 23 | flat?: boolean; 24 | path?: string; 25 | skipImport?: boolean; 26 | } 27 | 28 | /** 29 | * Find the module referred by a set of options passed to the schematics. 30 | */ 31 | export function findModuleFromOptions( 32 | host: Tree, 33 | options: ModuleOptions, 34 | ): Path | undefined { 35 | if (options.hasOwnProperty('skipImport') && options.skipImport) { 36 | return undefined; 37 | } 38 | 39 | if (!options.module) { 40 | const pathToCheck = 41 | (options.path || '') + 42 | (options.flat ? '' : '/' + strings.dasherize(options.name)); 43 | 44 | return normalize(findModule(host, pathToCheck)); 45 | } else { 46 | const modulePath = normalize('/' + options.path + '/' + options.module); 47 | const moduleBaseName = normalize(modulePath).split('/').pop(); 48 | 49 | if (host.exists(modulePath)) { 50 | return normalize(modulePath); 51 | } else if (host.exists(modulePath + '.ts')) { 52 | return normalize(modulePath + '.ts'); 53 | } else if (host.exists(modulePath + '.module.ts')) { 54 | return normalize(modulePath + '.module.ts'); 55 | } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { 56 | return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); 57 | } else { 58 | throw new Error(`Specified module path ${modulePath} does not exist`); 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Function to find the "closest" module to a generated file's path. 65 | */ 66 | export function findModule(host: Tree, generateDir: string): Path { 67 | let dir: DirEntry | null = host.getDir('/' + generateDir); 68 | 69 | const moduleRe = /\.module\.ts$/; 70 | const routingModuleRe = /-routing\.module\.ts/; 71 | 72 | while (dir) { 73 | const matches = dir.subfiles.filter( 74 | (p) => moduleRe.test(p) && !routingModuleRe.test(p), 75 | ); 76 | 77 | if (matches.length == 1) { 78 | return join(dir.path, matches[0]); 79 | } else if (matches.length > 1) { 80 | throw new Error( 81 | 'More than one module matches. Use skip-import option to skip importing ' + 82 | 'the component into the closest module.', 83 | ); 84 | } 85 | 86 | dir = dir.parent; 87 | } 88 | 89 | throw new Error( 90 | 'Could not find an NgModule. Use the skip-import ' + 91 | 'option to skip importing in NgModule.', 92 | ); 93 | } 94 | 95 | /** 96 | * Build a relative path from one file path to another file path. 97 | */ 98 | export function buildRelativePath(from: string, to: string): string { 99 | const { 100 | path: fromPath, 101 | filename: fromFileName, 102 | directory: fromDirectory, 103 | } = parsePath(from); 104 | const { 105 | path: toPath, 106 | filename: toFileName, 107 | directory: toDirectory, 108 | } = parsePath(to); 109 | const relativePath = relative(fromDirectory, toDirectory); 110 | const fixedRelativePath = relativePath.startsWith('.') 111 | ? relativePath 112 | : `./${relativePath}`; 113 | 114 | return !toFileName || toFileName === 'index.ts' 115 | ? fixedRelativePath 116 | : `${ 117 | fixedRelativePath.endsWith('/') 118 | ? fixedRelativePath 119 | : fixedRelativePath + '/' 120 | }${convertToTypeScriptFileName(toFileName)}`; 121 | } 122 | 123 | function parsePath(path: string) { 124 | const pathNormalized = normalize(path) as Path; 125 | const filename = extname(pathNormalized) ? basename(pathNormalized) : ''; 126 | const directory = filename ? dirname(pathNormalized) : pathNormalized; 127 | return { 128 | path: pathNormalized, 129 | filename, 130 | directory, 131 | }; 132 | } 133 | /** 134 | * Strips the typescript extension and clears index filenames 135 | * foo.ts -> foo 136 | * index.ts -> empty 137 | */ 138 | function convertToTypeScriptFileName(filename: string | undefined) { 139 | return filename ? filename.replace(/(\.ts)|(index\.ts)$/, '') : ''; 140 | } 141 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/json-utilts.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/json-utils.ts 2 | export function findPropertyInAstObject( 3 | node: any, 4 | propertyName: string, 5 | ): any | null { 6 | let maybeNode: any | null = null; 7 | for (const property of node.properties) { 8 | if (property.key.value == propertyName) { 9 | maybeNode = property.value; 10 | } 11 | } 12 | 13 | return maybeNode; 14 | } 15 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/package.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | 3 | /** 4 | * Adds a package to the package.json 5 | */ 6 | export function addPackageToPackageJson( 7 | host: Tree, 8 | type: string, 9 | pkg: string, 10 | version: string, 11 | ): Tree { 12 | if (host.exists('package.json')) { 13 | const sourceText = host.read('package.json')?.toString('utf-8') ?? '{}'; 14 | const json = JSON.parse(sourceText); 15 | if (!json[type]) { 16 | json[type] = {}; 17 | } 18 | 19 | if (!json[type][pkg]) { 20 | json[type][pkg] = version; 21 | } 22 | 23 | host.overwrite('package.json', JSON.stringify(json, null, 2)); 24 | } 25 | 26 | return host; 27 | } 28 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/parse-name.ts: -------------------------------------------------------------------------------- 1 | import { Path, basename, dirname, normalize } from '@angular-devkit/core'; 2 | 3 | export interface Location { 4 | name: string; 5 | path: Path; 6 | } 7 | 8 | export function parseName(path: string, name: string): Location { 9 | const nameWithoutPath = basename(name as Path); 10 | const namePath = dirname((path + '/' + name) as Path); 11 | 12 | return { 13 | name: nameWithoutPath, 14 | path: normalize('/' + namePath), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/project.ts: -------------------------------------------------------------------------------- 1 | // import { TargetDefinition } from '@angular-devkit/core/src/workspace'; 2 | import { getWorkspace } from './config'; 3 | import { SchematicsException, Tree } from '@angular-devkit/schematics'; 4 | 5 | export interface WorkspaceProject { 6 | root: string; 7 | projectType: string; 8 | architect: { 9 | // [key: string]: TargetDefinition; 10 | [key: string]: any; 11 | }; 12 | } 13 | 14 | export function getProject( 15 | host: Tree, 16 | options: { project?: string | undefined; path?: string | undefined }, 17 | ): WorkspaceProject { 18 | const workspace = getWorkspace(host); 19 | 20 | if (!options.project) { 21 | const defaultProject = (workspace as { defaultProject?: string }) 22 | .defaultProject; 23 | options.project = 24 | defaultProject !== undefined 25 | ? defaultProject 26 | : Object.keys(workspace.projects)[0]; 27 | } 28 | 29 | return workspace.projects[options.project]; 30 | } 31 | 32 | export function getProjectPath( 33 | host: Tree, 34 | options: { project?: string | undefined; path?: string | undefined }, 35 | ) { 36 | const project = getProject(host, options); 37 | 38 | if (project.root.slice(-1) === '/') { 39 | project.root = project.root.substring(0, project.root.length - 1); 40 | } 41 | 42 | if (options.path === undefined) { 43 | const projectDirName = 44 | project.projectType === 'application' ? 'app' : 'lib'; 45 | 46 | return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; 47 | } 48 | 49 | return options.path; 50 | } 51 | 52 | export function isLib( 53 | host: Tree, 54 | options: { project?: string | undefined; path?: string | undefined }, 55 | ) { 56 | const project = getProject(host, options); 57 | 58 | return project.projectType === 'library'; 59 | } 60 | 61 | export function getProjectMainFile( 62 | host: Tree, 63 | options: { project?: string | undefined; path?: string | undefined }, 64 | ) { 65 | if (isLib(host, options)) { 66 | throw new SchematicsException(`Invalid project type`); 67 | } 68 | const project = getProject(host, options); 69 | const projectOptions = project.architect['build'].options; 70 | 71 | if (!projectOptions?.main && !projectOptions?.browser) { 72 | throw new SchematicsException(`Could not find the main file ${project}`); 73 | } 74 | 75 | return (projectOptions.browser || projectOptions.main) as string; 76 | } 77 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/strings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | const STRING_DASHERIZE_REGEXP = /[ _]/g; 9 | const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; 10 | const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; 11 | const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; 12 | const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; 13 | 14 | /** 15 | * Converts a camelized string into all lower case separated by underscores. 16 | * 17 | ```javascript 18 | decamelize('innerHTML'); // 'inner_html' 19 | decamelize('action_name'); // 'action_name' 20 | decamelize('css-class-name'); // 'css-class-name' 21 | decamelize('my favorite items'); // 'my favorite items' 22 | ``` 23 | */ 24 | export function decamelize(str: string): string { 25 | return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); 26 | } 27 | 28 | /** 29 | Replaces underscores, spaces, or camelCase with dashes. 30 | 31 | ```javascript 32 | dasherize('innerHTML'); // 'inner-html' 33 | dasherize('action_name'); // 'action-name' 34 | dasherize('css-class-name'); // 'css-class-name' 35 | dasherize('my favorite items'); // 'my-favorite-items' 36 | ``` 37 | */ 38 | export function dasherize(str?: string): string { 39 | return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); 40 | } 41 | 42 | /** 43 | Returns the lowerCamelCase form of a string. 44 | 45 | ```javascript 46 | camelize('innerHTML'); // 'innerHTML' 47 | camelize('action_name'); // 'actionName' 48 | camelize('css-class-name'); // 'cssClassName' 49 | camelize('my favorite items'); // 'myFavoriteItems' 50 | camelize('My Favorite Items'); // 'myFavoriteItems' 51 | ``` 52 | */ 53 | export function camelize(str: string): string { 54 | return str 55 | .replace( 56 | STRING_CAMELIZE_REGEXP, 57 | (_match: string, _separator: string, chr: string) => { 58 | return chr ? chr.toUpperCase() : ''; 59 | }, 60 | ) 61 | .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); 62 | } 63 | 64 | /** 65 | Returns the UpperCamelCase form of a string. 66 | 67 | ```javascript 68 | 'innerHTML'.classify(); // 'InnerHTML' 69 | 'action_name'.classify(); // 'ActionName' 70 | 'css-class-name'.classify(); // 'CssClassName' 71 | 'my favorite items'.classify(); // 'MyFavoriteItems' 72 | ``` 73 | */ 74 | export function classify(str: string): string { 75 | return str 76 | .split('.') 77 | .map((part) => capitalize(camelize(part))) 78 | .join('.'); 79 | } 80 | 81 | /** 82 | More general than decamelize. Returns the lower\_case\_and\_underscored 83 | form of a string. 84 | 85 | ```javascript 86 | 'innerHTML'.underscore(); // 'inner_html' 87 | 'action_name'.underscore(); // 'action_name' 88 | 'css-class-name'.underscore(); // 'css_class_name' 89 | 'my favorite items'.underscore(); // 'my_favorite_items' 90 | ``` 91 | */ 92 | export function underscore(str: string): string { 93 | return str 94 | .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') 95 | .replace(STRING_UNDERSCORE_REGEXP_2, '_') 96 | .toLowerCase(); 97 | } 98 | 99 | /** 100 | Returns the Capitalized form of a string 101 | 102 | ```javascript 103 | 'innerHTML'.capitalize() // 'InnerHTML' 104 | 'action_name'.capitalize() // 'Action_name' 105 | 'css-class-name'.capitalize() // 'Css-class-name' 106 | 'my favorite items'.capitalize() // 'My favorite items' 107 | ``` 108 | */ 109 | export function capitalize(str: string): string { 110 | return str.charAt(0).toUpperCase() + str.substring(1); 111 | } 112 | 113 | /** 114 | Returns the plural form of a string 115 | 116 | ```javascript 117 | 'innerHTML'.pluralize() // 'innerHTMLs' 118 | 'action_name'.pluralize() // 'actionNames' 119 | 'css-class-name'.pluralize() // 'cssClassNames' 120 | 'regex'.pluralize() // 'regexes' 121 | 'user'.pluralize() // 'users' 122 | ``` 123 | */ 124 | export function pluralize(str: string): string { 125 | return camelize( 126 | [/([^aeiou])y$/, /()fe?$/, /([^aeiou]o|[sxz]|[cs]h)$/].map( 127 | (c, i) => (str = str.replace(c, `$1${'iv'[i] || ''}e`)), 128 | ) && str + 's', 129 | ); 130 | } 131 | 132 | export function group(name: string, group: string | undefined) { 133 | return group ? `${group}/${name}` : name; 134 | } 135 | 136 | export function featurePath( 137 | group: boolean | undefined, 138 | flat: boolean | undefined, 139 | path: string, 140 | name: string, 141 | ) { 142 | if (group && !flat) { 143 | return `../../${path}/${name}/`; 144 | } 145 | 146 | return group ? `../${path}/` : './'; 147 | } 148 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/update.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/angular-redux/c934ad5a6f3d9d7f973db31a4bf0ed2c909e7166/projects/angular-redux/schematics-core/utility/update.ts -------------------------------------------------------------------------------- /projects/angular-redux/schematics-core/utility/visitors.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { normalize, resolve } from '@angular-devkit/core'; 3 | import { Tree, DirEntry } from '@angular-devkit/schematics'; 4 | 5 | export function visitTSSourceFiles( 6 | tree: Tree, 7 | visitor: ( 8 | sourceFile: ts.SourceFile, 9 | tree: Tree, 10 | result?: Result, 11 | ) => Result | undefined, 12 | ): Result | undefined { 13 | let result: Result | undefined = undefined; 14 | for (const sourceFile of visit(tree.root)) { 15 | result = visitor(sourceFile, tree, result); 16 | } 17 | 18 | return result; 19 | } 20 | 21 | export function visitTemplates( 22 | tree: Tree, 23 | visitor: ( 24 | template: { 25 | fileName: string; 26 | content: string; 27 | inline: boolean; 28 | start: number; 29 | }, 30 | tree: Tree, 31 | ) => void, 32 | ): void { 33 | visitTSSourceFiles(tree, (source) => { 34 | visitComponents(source, (_, decoratorExpressionNode) => { 35 | ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { 36 | if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { 37 | if ( 38 | n.name.text === 'template' && 39 | ts.isStringLiteralLike(n.initializer) 40 | ) { 41 | // Need to add an offset of one to the start because the template quotes are 42 | // not part of the template content. 43 | const templateStartIdx = n.initializer.getStart() + 1; 44 | visitor( 45 | { 46 | fileName: source.fileName, 47 | content: n.initializer.text, 48 | inline: true, 49 | start: templateStartIdx, 50 | }, 51 | tree, 52 | ); 53 | return; 54 | } else if ( 55 | n.name.text === 'templateUrl' && 56 | ts.isStringLiteralLike(n.initializer) 57 | ) { 58 | const parts = normalize(source.fileName).split('/').slice(0, -1); 59 | const templatePath = resolve( 60 | normalize(parts.join('/')), 61 | normalize(n.initializer.text), 62 | ); 63 | if (!tree.exists(templatePath)) { 64 | return; 65 | } 66 | 67 | const fileContent = tree.read(templatePath); 68 | if (!fileContent) { 69 | return; 70 | } 71 | 72 | visitor( 73 | { 74 | fileName: templatePath, 75 | content: fileContent.toString(), 76 | inline: false, 77 | start: 0, 78 | }, 79 | tree, 80 | ); 81 | return; 82 | } 83 | } 84 | 85 | ts.forEachChild(n, findTemplates); 86 | }); 87 | }); 88 | }); 89 | } 90 | 91 | export function visitNgModuleImports( 92 | sourceFile: ts.SourceFile, 93 | callback: ( 94 | importNode: ts.PropertyAssignment, 95 | elementExpressions: ts.NodeArray, 96 | ) => void, 97 | ) { 98 | visitNgModuleProperty(sourceFile, callback, 'imports'); 99 | } 100 | 101 | export function visitNgModuleExports( 102 | sourceFile: ts.SourceFile, 103 | callback: ( 104 | exportNode: ts.PropertyAssignment, 105 | elementExpressions: ts.NodeArray, 106 | ) => void, 107 | ) { 108 | visitNgModuleProperty(sourceFile, callback, 'exports'); 109 | } 110 | 111 | function visitNgModuleProperty( 112 | sourceFile: ts.SourceFile, 113 | callback: ( 114 | nodes: ts.PropertyAssignment, 115 | elementExpressions: ts.NodeArray, 116 | ) => void, 117 | property: string, 118 | ) { 119 | visitNgModules(sourceFile, (_, decoratorExpressionNode) => { 120 | ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { 121 | if ( 122 | ts.isPropertyAssignment(n) && 123 | ts.isIdentifier(n.name) && 124 | n.name.text === property && 125 | ts.isArrayLiteralExpression(n.initializer) 126 | ) { 127 | callback(n, n.initializer.elements); 128 | return; 129 | } 130 | 131 | ts.forEachChild(n, findTemplates); 132 | }); 133 | }); 134 | } 135 | export function visitComponents( 136 | sourceFile: ts.SourceFile, 137 | callback: ( 138 | classDeclarationNode: ts.ClassDeclaration, 139 | decoratorExpressionNode: ts.ObjectLiteralExpression, 140 | ) => void, 141 | ) { 142 | visitDecorator(sourceFile, 'Component', callback); 143 | } 144 | 145 | export function visitNgModules( 146 | sourceFile: ts.SourceFile, 147 | callback: ( 148 | classDeclarationNode: ts.ClassDeclaration, 149 | decoratorExpressionNode: ts.ObjectLiteralExpression, 150 | ) => void, 151 | ) { 152 | visitDecorator(sourceFile, 'NgModule', callback); 153 | } 154 | 155 | export function visitDecorator( 156 | sourceFile: ts.SourceFile, 157 | decoratorName: string, 158 | callback: ( 159 | classDeclarationNode: ts.ClassDeclaration, 160 | decoratorExpressionNode: ts.ObjectLiteralExpression, 161 | ) => void, 162 | ) { 163 | ts.forEachChild(sourceFile, function findClassDeclaration(node) { 164 | if (!ts.isClassDeclaration(node)) { 165 | ts.forEachChild(node, findClassDeclaration); 166 | } 167 | 168 | const classDeclarationNode = node as ts.ClassDeclaration; 169 | const decorators = ts.getDecorators(classDeclarationNode); 170 | 171 | if (!decorators || !decorators.length) { 172 | return; 173 | } 174 | 175 | const componentDecorator = decorators.find((d) => { 176 | return ( 177 | ts.isCallExpression(d.expression) && 178 | ts.isIdentifier(d.expression.expression) && 179 | d.expression.expression.text === decoratorName 180 | ); 181 | }); 182 | 183 | if (!componentDecorator) { 184 | return; 185 | } 186 | 187 | const { expression } = componentDecorator; 188 | if (!ts.isCallExpression(expression)) { 189 | return; 190 | } 191 | 192 | const [arg] = expression.arguments; 193 | if (!arg || !ts.isObjectLiteralExpression(arg)) { 194 | return; 195 | } 196 | 197 | callback(classDeclarationNode, arg); 198 | }); 199 | } 200 | 201 | function* visit(directory: DirEntry): IterableIterator { 202 | for (const path of directory.subfiles) { 203 | if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { 204 | const entry = directory.file(path); 205 | if (entry) { 206 | const content = entry.content; 207 | const source = ts.createSourceFile( 208 | entry.path, 209 | content.toString().replace(/^\uFEFF/, ''), 210 | ts.ScriptTarget.Latest, 211 | true, 212 | ); 213 | yield source; 214 | } 215 | } 216 | } 217 | 218 | for (const path of directory.subdirs) { 219 | if (path === 'node_modules') { 220 | continue; 221 | } 222 | 223 | yield* visit(directory.dir(path)); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "schematics": { 3 | "ng-add": { 4 | "aliases": ["init"], 5 | "factory": "./ng-add", 6 | "schema": "./ng-add/schema.json", 7 | "description": "Adds initial setup for state managment" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/jest.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | displayName: "Schematics", 3 | coverageDirectory: "../../coverage/modules/schematics", 4 | transform: { 5 | "^.+\\.(ts|mjs|js)$": [ 6 | "ts-jest", 7 | { 8 | tsconfig: "/tsconfig.spec.json", 9 | }, 10 | ], 11 | }, 12 | transformIgnorePatterns: ["node_modules/(?!.*\\.mjs$)"], 13 | }; 14 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/ng-add/files/__storePath__/counter-slice.ts.template: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | export interface CounterState { 5 | value: number 6 | } 7 | 8 | const initialState: CounterState = { 9 | value: 0, 10 | } 11 | 12 | export const counterSlice = createSlice({ 13 | name: 'counter', 14 | initialState, 15 | reducers: { 16 | increment: (state) => { 17 | state.value += 1 18 | }, 19 | decrement: (state) => { 20 | state.value -= 1 21 | }, 22 | incrementByAmount: (state, action: PayloadAction) => { 23 | state.value += action.payload 24 | }, 25 | }, 26 | }) 27 | 28 | export const { increment, decrement, incrementByAmount } = counterSlice.actions 29 | 30 | export default counterSlice.reducer 31 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/ng-add/files/__storePath__/index.ts.template: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import counterReducer from './counter-slice' 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | counter: counterReducer, 7 | }, 8 | }) 9 | 10 | export type RootState = ReturnType 11 | export type AppDispatch = typeof store.dispatch 12 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/ng-add/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchematicTestRunner, 3 | UnitTestTree, 4 | } from '@angular-devkit/schematics/testing'; 5 | import * as path from 'path'; 6 | import { Schema as AngularReduxOptions } from './schema'; 7 | import { 8 | getTestProjectPath, 9 | createWorkspace, 10 | } from '@reduxjs/angular-redux/schematics-core/testing'; 11 | 12 | describe('Store ng-add Schematic', () => { 13 | const schematicRunner = new SchematicTestRunner( 14 | '@reduxjs/angular-redux', 15 | path.join(__dirname, '../collection.json'), 16 | ); 17 | const defaultOptions: AngularReduxOptions = { 18 | skipPackageJson: false, 19 | project: 'bar', 20 | module: 'app', 21 | }; 22 | 23 | const projectPath = getTestProjectPath(); 24 | let appTree: UnitTestTree; 25 | 26 | beforeEach(async () => { 27 | appTree = await createWorkspace(schematicRunner, appTree); 28 | }); 29 | 30 | it('should update package.json', async () => { 31 | const options = { ...defaultOptions }; 32 | 33 | const tree = await schematicRunner.runSchematic('ng-add', options, appTree); 34 | 35 | const packageJson = JSON.parse(tree.readContent('/package.json')); 36 | 37 | expect(packageJson.dependencies['@reduxjs/angular-redux']).toBeDefined(); 38 | expect(packageJson.dependencies['redux']).toBeDefined(); 39 | expect(packageJson.dependencies['@reduxjs/toolkit']).toBeDefined(); 40 | }); 41 | 42 | it('should skip package.json update', async () => { 43 | const options = { ...defaultOptions, skipPackageJson: true }; 44 | 45 | const tree = await schematicRunner.runSchematic('ng-add', options, appTree); 46 | const packageJson = JSON.parse(tree.readContent('/package.json')); 47 | 48 | expect(packageJson.dependencies['@reduxjs/angular-redux']).toBeUndefined(); 49 | expect(packageJson.dependencies['redux']).toBeUndefined(); 50 | expect(packageJson.dependencies['@reduxjs/toolkit']).toBeUndefined(); 51 | }); 52 | 53 | it('should create the initial store setup', async () => { 54 | const options = { ...defaultOptions }; 55 | 56 | const tree = await schematicRunner.runSchematic('ng-add', options, appTree); 57 | const files = tree.files; 58 | expect( 59 | files.indexOf(`${projectPath}/src/app/store/index.ts`), 60 | ).toBeGreaterThanOrEqual(0); 61 | }); 62 | 63 | it('should import into a specified module', async () => { 64 | const options = { ...defaultOptions }; 65 | 66 | const tree = await schematicRunner.runSchematic('ng-add', options, appTree); 67 | const content = tree.readContent(`${projectPath}/src/app/app.module.ts`); 68 | expect(content).toMatch(/import { store } from '\.\/store';/); 69 | }); 70 | 71 | it('should fail if specified module does not exist', async () => { 72 | const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' }; 73 | let thrownError: Error | null = null; 74 | try { 75 | await schematicRunner.runSchematic('ng-add', options, appTree); 76 | } catch (err: any) { 77 | thrownError = err; 78 | } 79 | expect(thrownError).toBeDefined(); 80 | }); 81 | 82 | describe('Store ng-add Schematic for standalone application', () => { 83 | const projectPath = getTestProjectPath(undefined, { 84 | name: 'bar-standalone', 85 | }); 86 | const standaloneDefaultOptions = { 87 | ...defaultOptions, 88 | project: 'bar-standalone', 89 | }; 90 | 91 | it('provides minimal store setup', async () => { 92 | const options = { ...standaloneDefaultOptions, minimal: true }; 93 | const tree = await schematicRunner.runSchematic( 94 | 'ng-add', 95 | options, 96 | appTree, 97 | ); 98 | 99 | const content = tree.readContent(`${projectPath}/src/app/app.config.ts`); 100 | const files = tree.files; 101 | 102 | expect(content).toMatch(/provideRedux\(\{ store \}\)/); 103 | expect(content).toMatch(/import { store } from '\.\/store';/); 104 | expect(files.indexOf(`${projectPath}/src/app/store/index.ts`)).not.toBe( 105 | -1, 106 | ); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/ng-add/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as ts from 'typescript'; 4 | import { 5 | Rule, 6 | SchematicContext, 7 | SchematicsException, 8 | Tree, 9 | apply, 10 | applyTemplates, 11 | branchAndMerge, 12 | chain, 13 | mergeWith, 14 | url, 15 | noop, 16 | move, 17 | filter, 18 | } from '@angular-devkit/schematics'; 19 | import { 20 | NodePackageInstallTask, 21 | RunSchematicTask, 22 | } from '@angular-devkit/schematics/tasks'; 23 | import { 24 | InsertChange, 25 | addImportToModule, 26 | buildRelativePath, 27 | findModuleFromOptions, 28 | getProjectPath, 29 | insertImport, 30 | stringUtils, 31 | addPackageToPackageJson, 32 | parseName, 33 | } from '../../schematics-core'; 34 | import { Schema as AngularReduxOptions } from './schema'; 35 | import { getProjectMainFile } from '../../schematics-core/utility/project'; 36 | import { 37 | addFunctionalProvidersToStandaloneBootstrap, 38 | callsProvidersFunction, 39 | } from '../../schematics-core/utility/standalone'; 40 | import { isStandaloneApp } from '@schematics/angular/utility/ng-ast-utils'; 41 | 42 | function addImportToNgModule(options: AngularReduxOptions): Rule { 43 | return (host: Tree) => { 44 | const modulePath = options.module; 45 | 46 | if (!modulePath) { 47 | return host; 48 | } 49 | 50 | if (!host.exists(modulePath)) { 51 | throw new Error('Specified module does not exist'); 52 | } 53 | 54 | const text = host.read(modulePath); 55 | if (text === null) { 56 | throw new SchematicsException(`File ${modulePath} does not exist.`); 57 | } 58 | const sourceText = text.toString('utf-8'); 59 | 60 | const source = ts.createSourceFile( 61 | modulePath, 62 | sourceText, 63 | ts.ScriptTarget.Latest, 64 | true, 65 | ); 66 | 67 | const storeModuleSetup = `provideRedux({store: store})`; 68 | 69 | const statePath = `/${options.path}/${options.storePath}`; 70 | const relativePath = buildRelativePath(modulePath, statePath); 71 | const [storeNgModuleImport] = addImportToModule( 72 | source, 73 | modulePath, 74 | storeModuleSetup, 75 | relativePath, 76 | ); 77 | 78 | let changes = [ 79 | insertImport( 80 | source, 81 | modulePath, 82 | 'provideRedux', 83 | '@reduxjs/angular-redux', 84 | ), 85 | insertImport(source, modulePath, 'store', relativePath), 86 | storeNgModuleImport, 87 | ]; 88 | 89 | const recorder = host.beginUpdate(modulePath); 90 | 91 | for (const change of changes) { 92 | if (change instanceof InsertChange) { 93 | recorder.insertLeft(change.pos, change.toAdd); 94 | } 95 | } 96 | host.commitUpdate(recorder); 97 | 98 | return host; 99 | }; 100 | } 101 | 102 | const angularReduxPackageMeta = JSON.parse( 103 | fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8'), 104 | ) as unknown as { 105 | version: string; 106 | peerDependencies: { 107 | [key: string]: string; 108 | }; 109 | }; 110 | 111 | function addReduxDepsToPackageJson() { 112 | return (host: Tree, context: SchematicContext) => { 113 | addPackageToPackageJson( 114 | host, 115 | 'dependencies', 116 | '@reduxjs/toolkit', 117 | angularReduxPackageMeta.peerDependencies['@reduxjs/toolkit'], 118 | ); 119 | addPackageToPackageJson( 120 | host, 121 | 'dependencies', 122 | 'redux', 123 | angularReduxPackageMeta.peerDependencies['redux'], 124 | ); 125 | addPackageToPackageJson( 126 | host, 127 | 'dependencies', 128 | '@reduxjs/angular-redux', 129 | angularReduxPackageMeta.version, 130 | ); 131 | context.addTask(new NodePackageInstallTask()); 132 | return host; 133 | }; 134 | } 135 | 136 | function addStandaloneConfig(options: AngularReduxOptions): Rule { 137 | return (host: Tree) => { 138 | const mainFile = getProjectMainFile(host, options); 139 | 140 | if (host.exists(mainFile)) { 141 | const storeProviderFn = 'provideRedux'; 142 | 143 | if (callsProvidersFunction(host, mainFile, storeProviderFn)) { 144 | // exit because the store config is already provided 145 | return host; 146 | } 147 | const storeProviderOptions = [ts.factory.createIdentifier('{ store }')]; 148 | const patchedConfigFile = addFunctionalProvidersToStandaloneBootstrap( 149 | host, 150 | mainFile, 151 | storeProviderFn, 152 | '@reduxjs/angular-redux', 153 | storeProviderOptions, 154 | ); 155 | 156 | // insert reducers import into the patched file 157 | const configFileContent = host.read(patchedConfigFile); 158 | const source = ts.createSourceFile( 159 | patchedConfigFile, 160 | configFileContent?.toString('utf-8') || '', 161 | ts.ScriptTarget.Latest, 162 | true, 163 | ); 164 | const statePath = `/${options.path}/${options.storePath}`; 165 | const relativePath = buildRelativePath( 166 | `/${patchedConfigFile}`, 167 | statePath, 168 | ); 169 | 170 | const recorder = host.beginUpdate(patchedConfigFile); 171 | 172 | const change = insertImport( 173 | source, 174 | patchedConfigFile, 175 | 'store', 176 | relativePath, 177 | ); 178 | 179 | if (change instanceof InsertChange) { 180 | recorder.insertLeft(change.pos, change.toAdd); 181 | } 182 | 183 | host.commitUpdate(recorder); 184 | 185 | return host; 186 | } 187 | throw new SchematicsException( 188 | `Main file not found for a project ${options.project}`, 189 | ); 190 | }; 191 | } 192 | 193 | export default function (options: AngularReduxOptions): Rule { 194 | return (host: Tree, context: SchematicContext) => { 195 | const mainFile = getProjectMainFile(host, options); 196 | const isStandalone = isStandaloneApp(host, mainFile); 197 | 198 | options.path = getProjectPath(host, options); 199 | 200 | const parsedPath = parseName(options.path, ''); 201 | options.path = parsedPath.path; 202 | 203 | if (options.module && !isStandalone) { 204 | options.module = findModuleFromOptions(host, { 205 | name: '', 206 | module: options.module, 207 | path: options.path, 208 | }); 209 | } 210 | 211 | const templateSource = apply(url('./files'), [ 212 | applyTemplates({ 213 | ...stringUtils, 214 | ...options, 215 | }), 216 | move(parsedPath.path), 217 | ]); 218 | 219 | const configOrModuleUpdate = isStandalone 220 | ? addStandaloneConfig(options) 221 | : addImportToNgModule(options); 222 | 223 | return chain([ 224 | branchAndMerge(chain([configOrModuleUpdate, mergeWith(templateSource)])), 225 | options && options.skipPackageJson ? noop() : addReduxDepsToPackageJson(), 226 | ])(host, context); 227 | }; 228 | } 229 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/ng-add/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "SchematicsAngularRedux", 4 | "title": "Angular Redux Options Schema", 5 | "type": "object", 6 | "properties": { 7 | "skipPackageJson": { 8 | "type": "boolean", 9 | "default": false, 10 | "description": "Do not add Redux packages as dependencies to package.json (e.g., --skipPackageJson)." 11 | }, 12 | "path": { 13 | "type": "string", 14 | "format": "path", 15 | "description": "The path to create the state.", 16 | "visible": false, 17 | "$default": { 18 | "$source": "workingDirectory" 19 | } 20 | }, 21 | "project": { 22 | "type": "string", 23 | "description": "The name of the project.", 24 | "aliases": ["p"] 25 | }, 26 | "module": { 27 | "type": "string", 28 | "default": "app", 29 | "description": "Allows specification of the declaring module.", 30 | "alias": "m", 31 | "subtype": "filepath" 32 | }, 33 | "storePath": { 34 | "type": "string", 35 | "default": "store" 36 | } 37 | }, 38 | "required": [] 39 | } 40 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/ng-add/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | skipPackageJson?: boolean; 3 | path?: string; 4 | project?: string; 5 | module?: string; 6 | storePath?: string; 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular-redux/schematics/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "emitDecoratorMetadata": true, 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/inject-dispatch.ts: -------------------------------------------------------------------------------- 1 | import type { Dispatch, UnknownAction, Action } from 'redux'; 2 | import { assertInInjectionContext } from '@angular/core'; 3 | import { injectStore } from './inject-store'; 4 | 5 | /** 6 | * Represents a custom injection that provides a dispatch function 7 | * from the Redux store. 8 | * 9 | * @template DispatchType - The specific type of the dispatch function. 10 | * 11 | * @public 12 | */ 13 | export interface InjectDispatch< 14 | DispatchType extends Dispatch = Dispatch, 15 | > { 16 | /** 17 | * Returns the dispatch function from the Redux store. 18 | * 19 | * @returns The dispatch function from the Redux store. 20 | * 21 | * @template AppDispatch - The specific type of the dispatch function. 22 | */ 23 | (): AppDispatch; 24 | 25 | /** 26 | * Creates a "pre-typed" version of {@linkcode injectDispatch injectDispatch} 27 | * where the type of the `dispatch` function is predefined. 28 | * 29 | * This allows you to set the `dispatch` type once, eliminating the need to 30 | * specify it with every {@linkcode injectDispatch injectDispatch} call. 31 | * 32 | * @returns A pre-typed `injectDispatch` with the dispatch type already defined. 33 | * 34 | * @example 35 | * ```ts 36 | * export const injectAppDispatch = injectDispatch.withTypes() 37 | * ``` 38 | * 39 | * @template OverrideDispatchType - The specific type of the dispatch function. 40 | */ 41 | withTypes: < 42 | OverrideDispatchType extends DispatchType, 43 | >() => InjectDispatch; 44 | } 45 | 46 | /** 47 | * Injection factory, which creates a `injectDispatch` injection bound to a given context. 48 | * 49 | * @returns {Function} A `injectDispatch` injection bound to the specified context. 50 | */ 51 | export function createDispatchInjection< 52 | ActionType extends Action = UnknownAction, 53 | >() { 54 | const injectDispatch = < 55 | AppDispatch extends Dispatch = Dispatch, 56 | >(): AppDispatch => { 57 | assertInInjectionContext(injectDispatch); 58 | const store = injectStore(); 59 | return store.dispatch as AppDispatch; 60 | }; 61 | 62 | Object.assign(injectDispatch, { 63 | withTypes: () => injectDispatch, 64 | }); 65 | 66 | return injectDispatch as InjectDispatch>; 67 | } 68 | 69 | /** 70 | * A injection to access the redux `dispatch` function. 71 | * 72 | * @returns {any|function} redux store's `dispatch` function 73 | * 74 | * @example 75 | * 76 | * import { injectDispatch } from '@reduxjs/angular-redux' 77 | * 78 | * @Component({ 79 | * selector: 'example-component', 80 | * template: ` 81 | *
82 | * {{value}} 83 | * 84 | *
85 | * ` 86 | * }) 87 | * export class CounterComponent { 88 | * dispatch = injectDispatch() 89 | * increaseCounter = () => dispatch({ type: 'increase-counter' }) 90 | * } 91 | */ 92 | export const injectDispatch = /* #__PURE__*/ createDispatchInjection(); 93 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/inject-selector.ts: -------------------------------------------------------------------------------- 1 | import { EqualityFn } from './types'; 2 | import { 3 | assertInInjectionContext, 4 | DestroyRef, 5 | effect, 6 | inject, 7 | linkedSignal, 8 | Signal, 9 | signal, 10 | } from '@angular/core'; 11 | import { ReduxProvider } from './provider'; 12 | 13 | export interface InjectSelectorOptions { 14 | equalityFn?: EqualityFn; 15 | } 16 | 17 | const refEquality: EqualityFn = (a, b) => a === b; 18 | 19 | /** 20 | * Represents a custom injection that allows you to extract data from the 21 | * Redux store state, using a selector function. The selector function 22 | * takes the current state as an argument and returns a part of the state 23 | * or some derived data. The injection also supports an optional equality 24 | * function or options object to customize its behavior. 25 | * 26 | * @template StateType - The specific type of state this injection operates on. 27 | * 28 | * @public 29 | */ 30 | export interface InjectSelector { 31 | /** 32 | * A function that takes a selector function as its first argument. 33 | * The selector function is responsible for selecting a part of 34 | * the Redux store's state or computing derived data. 35 | * 36 | * @param selector - A function that receives the current state and returns a part of the state or some derived data. 37 | * @param equalityFnOrOptions - An optional equality function or options object for customizing the behavior of the selector. 38 | * @returns The selected part of the state or derived data. 39 | * 40 | * @template TState - The specific type of state this injection operates on. 41 | * @template Selected - The type of the value that the selector function will return. 42 | */ 43 | ( 44 | selector: (state: TState) => Selected, 45 | equalityFnOrOptions?: 46 | | EqualityFn 47 | | InjectSelectorOptions, 48 | ): Signal; 49 | 50 | /** 51 | * Creates a "pre-typed" version of {@linkcode injectSelector injectSelector} 52 | * where the `state` type is predefined. 53 | * 54 | * This allows you to set the `state` type once, eliminating the need to 55 | * specify it with every {@linkcode injectSelector injectSelector} call. 56 | * 57 | * @returns A pre-typed `injectSelector` with the state type already defined. 58 | * 59 | * @example 60 | * ```ts 61 | * export const injectAppSelector = injectSelector.withTypes() 62 | * ``` 63 | * 64 | * @template OverrideStateType - The specific type of state this injection operates on. 65 | */ 66 | withTypes: < 67 | OverrideStateType extends StateType, 68 | >() => InjectSelector; 69 | } 70 | 71 | /** 72 | * Injection factory, which creates a `injectSelector` injection bound to a given context. 73 | * 74 | * @returns {Function} A `injectSelector` injection bound to the specified context. 75 | */ 76 | export function createSelectorInjection(): InjectSelector { 77 | const injectSelector = ( 78 | selector: (state: TState) => Selected, 79 | equalityFnOrOptions: 80 | | EqualityFn 81 | | InjectSelectorOptions = {}, 82 | ): Signal => { 83 | assertInInjectionContext(injectSelector); 84 | const reduxContext = inject(ReduxProvider); 85 | const destroyRef = inject(DestroyRef); 86 | 87 | const { equalityFn = refEquality } = 88 | typeof equalityFnOrOptions === 'function' 89 | ? { equalityFn: equalityFnOrOptions } 90 | : equalityFnOrOptions; 91 | 92 | const { store, subscription } = reduxContext; 93 | 94 | const selectedState = linkedSignal(() => selector(store.getState()), { 95 | equal: equalityFn, 96 | }); 97 | 98 | const unsubscribe = subscription.addNestedSub(() => { 99 | const data = selector(store.getState()); 100 | 101 | selectedState.set(data); 102 | }); 103 | 104 | destroyRef.onDestroy(() => { 105 | unsubscribe(); 106 | }); 107 | 108 | return selectedState.asReadonly(); 109 | }; 110 | 111 | Object.assign(injectSelector, { 112 | withTypes: () => injectSelector, 113 | }); 114 | 115 | return injectSelector as InjectSelector; 116 | } 117 | 118 | /** 119 | * A injection to access the redux store's state. This injection takes a selector function 120 | * as an argument. The selector is called with the store state. 121 | * 122 | * This injection takes an optional equality comparison function as the second parameter 123 | * that allows you to customize the way the selected state is compared to determine 124 | * whether the component needs to be re-rendered. 125 | * 126 | * @param {Function} selector the selector function 127 | * @param {Function=} equalityFn the function that will be used to determine equality 128 | * 129 | * @returns {any} the selected state 130 | * 131 | * @example 132 | * 133 | * import { injectSelector } from '@reduxjs/angular-redux' 134 | * 135 | * @Component({ 136 | * selector: 'counter-component', 137 | * template: `
{{counter}}
` 138 | * }) 139 | * export class CounterComponent { 140 | * counter = injectSelector(state => state.counter) 141 | * } 142 | */ 143 | export const injectSelector = /* #__PURE__*/ createSelectorInjection(); 144 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/inject-store.ts: -------------------------------------------------------------------------------- 1 | import { assertInInjectionContext, inject } from '@angular/core'; 2 | import { ReduxProvider } from './provider'; 3 | import type { Store, Action } from 'redux'; 4 | 5 | /** 6 | * Represents a type that extracts the action type from a given Redux store. 7 | * 8 | * @template StoreType - The specific type of the Redux store. 9 | * 10 | * @internal 11 | */ 12 | export type ExtractStoreActionType = 13 | StoreType extends Store ? ActionType : never; 14 | 15 | /** 16 | * Represents a custom injection that provides access to the Redux store. 17 | * 18 | * @template StoreType - The specific type of the Redux store that gets returned. 19 | * 20 | * @public 21 | */ 22 | export interface InjectStore { 23 | /** 24 | * Returns the Redux store instance. 25 | * 26 | * @returns The Redux store instance. 27 | */ 28 | (): StoreType; 29 | 30 | /** 31 | * Returns the Redux store instance with specific state and action types. 32 | * 33 | * @returns The Redux store with the specified state and action types. 34 | * 35 | * @template StateType - The specific type of the state used in the store. 36 | * @template ActionType - The specific type of the actions used in the store. 37 | */ 38 | < 39 | StateType extends ReturnType = ReturnType< 40 | StoreType['getState'] 41 | >, 42 | ActionType extends Action = ExtractStoreActionType, 43 | >(): Store; 44 | 45 | /** 46 | * Creates a "pre-typed" version of {@linkcode injectStore injectStore} 47 | * where the type of the Redux `store` is predefined. 48 | * 49 | * This allows you to set the `store` type once, eliminating the need to 50 | * specify it with every {@linkcode injectStore injectStore} call. 51 | * 52 | * @returns A pre-typed `injectStore` with the store type already defined. 53 | * 54 | * @example 55 | * ```ts 56 | * export const useAppStore = injectStore.withTypes() 57 | * ``` 58 | * 59 | * @template OverrideStoreType - The specific type of the Redux store that gets returned. 60 | */ 61 | withTypes: < 62 | OverrideStoreType extends StoreType, 63 | >() => InjectStore; 64 | } 65 | 66 | /** 67 | * Injection factory, which creates a `injectStore` injection bound to a given context. 68 | * 69 | * @returns {Function} A `injectStore` injection bound to the specified context. 70 | */ 71 | export function createStoreInjection< 72 | StateType = unknown, 73 | ActionType extends Action = Action, 74 | >() { 75 | const injectStore = () => { 76 | assertInInjectionContext(injectStore); 77 | const context = inject(ReduxProvider); 78 | const { store } = context; 79 | return store; 80 | }; 81 | 82 | Object.assign(injectStore, { 83 | withTypes: () => injectStore, 84 | }); 85 | 86 | return injectStore as InjectStore>; 87 | } 88 | 89 | /** 90 | * A injection to access the redux store. 91 | * 92 | * @returns {any} the redux store 93 | * 94 | * @example 95 | * 96 | * import { injectStore } from '@reduxjs/angular-redux' 97 | * 98 | * @Component({ 99 | * selector: 'example-component', 100 | * template: `
{{store.getState()}}
` 101 | * }) 102 | * export class CounterComponent { 103 | * store = injectStore() 104 | * } 105 | */ 106 | export const injectStore = /* #__PURE__*/ createStoreInjection(); 107 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/provide-redux.ts: -------------------------------------------------------------------------------- 1 | import type { Action, Store, UnknownAction } from 'redux'; 2 | import { createReduxProvider, ReduxProvider } from './provider'; 3 | 4 | export interface ProviderProps< 5 | A extends Action = UnknownAction, 6 | S = unknown, 7 | > { 8 | /** 9 | * The single Redux store in your application. 10 | */ 11 | store: Store; 12 | } 13 | 14 | export function provideRedux< 15 | A extends Action = UnknownAction, 16 | S = unknown, 17 | >({ store }: ProviderProps) { 18 | return { 19 | provide: ReduxProvider, 20 | useValue: createReduxProvider(store), 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnDestroy } from '@angular/core'; 2 | import type { Action, Store, UnknownAction } from 'redux'; 3 | import { createSubscription } from './utils/Subscription'; 4 | 5 | @Injectable({ providedIn: null }) 6 | export class ReduxProvider< 7 | A extends Action = UnknownAction, 8 | S = unknown, 9 | > implements OnDestroy 10 | { 11 | store!: Store; 12 | subscription!: ReturnType; 13 | 14 | ngOnDestroy() { 15 | this.subscription.tryUnsubscribe(); 16 | this.subscription.onStateChange = undefined; 17 | } 18 | } 19 | 20 | // TODO: Ideally this runs in the constructor, but DI doesn't allow us to pass items to the constructor? 21 | export function createReduxProvider< 22 | A extends Action = UnknownAction, 23 | S = unknown, 24 | >(store: Store) { 25 | const provider = new ReduxProvider(); 26 | provider.store = store; 27 | const subscription = createSubscription(store); 28 | provider.subscription = subscription; 29 | subscription.onStateChange = subscription.notifyNestedSubs; 30 | subscription.trySubscribe(); 31 | 32 | return provider; 33 | } 34 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type EqualityFn = (a: T, b: T) => boolean; 2 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/utils/Subscription.ts: -------------------------------------------------------------------------------- 1 | // encapsulates the subscription logic for connecting a component to the redux store, as 2 | // well as nesting subscriptions of descendant components, so that we can ensure the 3 | // ancestor components re-render before descendants 4 | 5 | type VoidFunc = () => void; 6 | 7 | type Listener = { 8 | callback: VoidFunc; 9 | next: Listener | null; 10 | prev: Listener | null; 11 | }; 12 | 13 | function createListenerCollection() { 14 | let first: Listener | null = null; 15 | let last: Listener | null = null; 16 | 17 | return { 18 | clear() { 19 | first = null; 20 | last = null; 21 | }, 22 | 23 | notify() { 24 | let listener = first; 25 | while (listener) { 26 | listener.callback(); 27 | listener = listener.next; 28 | } 29 | }, 30 | 31 | get() { 32 | const listeners: Listener[] = []; 33 | let listener = first; 34 | while (listener) { 35 | listeners.push(listener); 36 | listener = listener.next; 37 | } 38 | return listeners; 39 | }, 40 | 41 | subscribe(callback: () => void) { 42 | let isSubscribed = true; 43 | 44 | const listener: Listener = (last = { 45 | callback, 46 | next: null, 47 | prev: last, 48 | }); 49 | 50 | if (listener.prev) { 51 | listener.prev.next = listener; 52 | } else { 53 | first = listener; 54 | } 55 | 56 | return function unsubscribe() { 57 | if (!isSubscribed || first === null) return; 58 | isSubscribed = false; 59 | 60 | if (listener.next) { 61 | listener.next.prev = listener.prev; 62 | } else { 63 | last = listener.prev; 64 | } 65 | if (listener.prev) { 66 | listener.prev.next = listener.next; 67 | } else { 68 | first = listener.next; 69 | } 70 | }; 71 | }, 72 | }; 73 | } 74 | 75 | type ListenerCollection = ReturnType; 76 | 77 | export interface Subscription { 78 | addNestedSub: (listener: VoidFunc) => VoidFunc; 79 | notifyNestedSubs: VoidFunc; 80 | handleChangeWrapper: VoidFunc; 81 | isSubscribed: () => boolean; 82 | onStateChange?: VoidFunc | null; 83 | trySubscribe: VoidFunc; 84 | tryUnsubscribe: VoidFunc; 85 | getListeners: () => ListenerCollection; 86 | } 87 | 88 | const nullListeners = { 89 | notify() {}, 90 | get: () => [], 91 | } as unknown as ListenerCollection; 92 | 93 | export function createSubscription(store: any, parentSub?: Subscription) { 94 | let unsubscribe: VoidFunc | undefined; 95 | let listeners: ListenerCollection = nullListeners; 96 | 97 | // Reasons to keep the subscription active 98 | let subscriptionsAmount = 0; 99 | 100 | // Is this specific subscription subscribed (or only nested ones?) 101 | let selfSubscribed = false; 102 | 103 | function addNestedSub(listener: () => void) { 104 | trySubscribe(); 105 | 106 | const cleanupListener = listeners.subscribe(listener); 107 | 108 | // cleanup nested sub 109 | let removed = false; 110 | return () => { 111 | if (!removed) { 112 | removed = true; 113 | cleanupListener(); 114 | tryUnsubscribe(); 115 | } 116 | }; 117 | } 118 | 119 | function notifyNestedSubs() { 120 | listeners.notify(); 121 | } 122 | 123 | function handleChangeWrapper() { 124 | if (subscription.onStateChange) { 125 | subscription.onStateChange(); 126 | } 127 | } 128 | 129 | function isSubscribed() { 130 | return selfSubscribed; 131 | } 132 | 133 | function trySubscribe() { 134 | subscriptionsAmount++; 135 | if (!unsubscribe) { 136 | unsubscribe = parentSub 137 | ? parentSub.addNestedSub(handleChangeWrapper) 138 | : store.subscribe(handleChangeWrapper); 139 | 140 | listeners = createListenerCollection(); 141 | } 142 | } 143 | 144 | function tryUnsubscribe() { 145 | subscriptionsAmount--; 146 | if (unsubscribe && subscriptionsAmount === 0) { 147 | unsubscribe(); 148 | unsubscribe = undefined; 149 | listeners.clear(); 150 | listeners = nullListeners; 151 | } 152 | } 153 | 154 | function trySubscribeSelf() { 155 | if (!selfSubscribed) { 156 | selfSubscribed = true; 157 | trySubscribe(); 158 | } 159 | } 160 | 161 | function tryUnsubscribeSelf() { 162 | if (selfSubscribed) { 163 | selfSubscribed = false; 164 | tryUnsubscribe(); 165 | } 166 | } 167 | 168 | const subscription: Subscription = { 169 | addNestedSub, 170 | notifyNestedSubs, 171 | handleChangeWrapper, 172 | isSubscribed, 173 | trySubscribe: trySubscribeSelf, 174 | tryUnsubscribe: tryUnsubscribeSelf, 175 | getListeners: () => listeners, 176 | }; 177 | 178 | return subscription; 179 | } 180 | -------------------------------------------------------------------------------- /projects/angular-redux/src/lib/utils/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | function is(x: unknown, y: unknown) { 2 | if (x === y) { 3 | return x !== 0 || y !== 0 || 1 / x === 1 / y; 4 | } else { 5 | return x !== x && y !== y; 6 | } 7 | } 8 | 9 | export function shallowEqual(objA: any, objB: any) { 10 | if (is(objA, objB)) return true; 11 | 12 | if ( 13 | typeof objA !== 'object' || 14 | objA === null || 15 | typeof objB !== 'object' || 16 | objB === null 17 | ) { 18 | return false; 19 | } 20 | 21 | const keysA = Object.keys(objA); 22 | const keysB = Object.keys(objB); 23 | 24 | if (keysA.length !== keysB.length) return false; 25 | 26 | for (let i = 0; i < keysA.length; i++) { 27 | if ( 28 | !Object.prototype.hasOwnProperty.call(objB, keysA[i]) || 29 | !is(objA[keysA[i]], objB[keysA[i]]) 30 | ) { 31 | return false; 32 | } 33 | } 34 | 35 | return true; 36 | } 37 | -------------------------------------------------------------------------------- /projects/angular-redux/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of @reduxjs/angular-redux 3 | */ 4 | 5 | export * from './lib/inject-dispatch'; 6 | export * from './lib/inject-selector'; 7 | export * from './lib/inject-store'; 8 | export * from './lib/provide-redux'; 9 | export * from './lib/provider'; 10 | export * from './lib/utils/shallowEqual'; 11 | export type { Subscription } from './lib/utils/Subscription'; 12 | -------------------------------------------------------------------------------- /projects/angular-redux/src/tests/inject-dispatch.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import { injectDispatch, provideRedux } from '../public-api'; 3 | import { Component } from '@angular/core'; 4 | import { render } from '@testing-library/angular'; 5 | 6 | const store = createStore((c: number = 1): number => c + 1); 7 | const store2 = createStore((c: number = 1): number => c + 2); 8 | 9 | describe('injectDispatch', () => { 10 | it("returns the store's dispatch function", async () => { 11 | @Component({ 12 | selector: 'app-root', 13 | standalone: true, 14 | template: '

', 15 | }) 16 | class Testing { 17 | dispatch = injectDispatch(); 18 | } 19 | 20 | const result = await render(Testing, { 21 | providers: [provideRedux({ store })], 22 | }); 23 | 24 | expect(result.fixture.componentRef.instance.dispatch).toBe(store.dispatch); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /projects/angular-redux/src/tests/inject-selector-and-dispatch.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | import { render, waitFor } from '@testing-library/angular'; 3 | import '@testing-library/jest-dom'; 4 | import { configureStore, createSlice } from '@reduxjs/toolkit'; 5 | import { provideRedux, injectDispatch, injectSelector } from '../public-api'; 6 | import { userEvent } from '@testing-library/user-event'; 7 | import { createStore } from 'redux'; 8 | 9 | const user = userEvent.setup(); 10 | 11 | const counterSlice = createSlice({ 12 | name: 'counter', 13 | initialState: { 14 | value: 0, 15 | }, 16 | reducers: { 17 | increment: (state) => { 18 | state.value += 1; 19 | }, 20 | }, 21 | }); 22 | 23 | @Component({ 24 | selector: 'app-root', 25 | standalone: true, 26 | template: ` 27 |
28 |
29 | 32 | Count: {{ count() }} 33 |
34 |
35 | `, 36 | }) 37 | export class AppComponent { 38 | count = injectSelector((state: any) => state.counter.value); 39 | dispatch = injectDispatch(); 40 | increment = counterSlice.actions.increment; 41 | } 42 | 43 | it('injectSelector should work without reactivity', async () => { 44 | const store = configureStore({ 45 | reducer: { 46 | counter: counterSlice.reducer, 47 | }, 48 | }); 49 | 50 | const { getByText } = await render(AppComponent, { 51 | providers: [provideRedux({ store })], 52 | }); 53 | 54 | expect(getByText('Count: 0')).toBeInTheDocument(); 55 | }); 56 | 57 | it('injectSelector should work with reactivity', async () => { 58 | const store = configureStore({ 59 | reducer: { 60 | counter: counterSlice.reducer, 61 | }, 62 | }); 63 | 64 | const { getByText, getByLabelText } = await render(AppComponent, { 65 | providers: [provideRedux({ store })], 66 | }); 67 | 68 | expect(getByText('Count: 0')).toBeInTheDocument(); 69 | 70 | await user.click(getByLabelText('Increment value')); 71 | 72 | expect(getByText('Count: 1')).toBeInTheDocument(); 73 | }); 74 | 75 | it('should show a value dispatched during ngOnInit', async () => { 76 | const store = configureStore({ 77 | reducer: { 78 | counter: counterSlice.reducer, 79 | }, 80 | }); 81 | 82 | @Component({ 83 | selector: 'app-root', 84 | standalone: true, 85 | template: '

Count: {{count()}}

', 86 | }) 87 | class Comp { 88 | count = injectSelector((state: any) => state.counter.value); 89 | dispatch = injectDispatch(); 90 | increment = counterSlice.actions.increment; 91 | 92 | ngOnInit() { 93 | this.dispatch(this.increment()); 94 | } 95 | } 96 | 97 | const { getByText } = await render(Comp, { 98 | providers: [provideRedux({ store })], 99 | }); 100 | 101 | await waitFor(() => expect(getByText('Count: 1')).toBeInTheDocument()); 102 | }); 103 | 104 | it("should not throw an error on a required input passed to the selector's fn", async () => { 105 | const store = configureStore({ 106 | reducer: { 107 | counter: counterSlice.reducer, 108 | }, 109 | }); 110 | 111 | @Component({ 112 | selector: 'app-count-and-add', 113 | standalone: true, 114 | template: ` 115 | 118 |

Count: {{ count() }}

119 | `, 120 | }) 121 | class CountAndAdd { 122 | dispatch = injectDispatch(); 123 | increment = counterSlice.actions.increment; 124 | addBy = input.required(); 125 | count = injectSelector((state: any) => state.counter.value + this.addBy()); 126 | } 127 | 128 | @Component({ 129 | selector: 'app-root', 130 | imports: [CountAndAdd], 131 | standalone: true, 132 | template: '', 133 | }) 134 | class App {} 135 | 136 | const { getByText, getByLabelText } = await render(App, { 137 | providers: [provideRedux({ store })], 138 | }); 139 | 140 | await waitFor(() => expect(getByText('Count: 12')).toBeInTheDocument()); 141 | await user.click(getByLabelText('Increment value')); 142 | await waitFor(() => expect(getByText('Count: 13')).toBeInTheDocument()); 143 | }); 144 | -------------------------------------------------------------------------------- /projects/angular-redux/src/tests/inject-selector.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectSelector, 3 | injectSelector, 4 | provideRedux, 5 | ReduxProvider, 6 | shallowEqual, 7 | } from '../public-api'; 8 | import { Component, effect, inject } from '@angular/core'; 9 | import { render, waitFor } from '@testing-library/angular'; 10 | import { AnyAction, createStore, Store } from 'redux'; 11 | import '@testing-library/jest-dom'; 12 | import { Subscription } from '../lib/utils/Subscription'; 13 | 14 | type NormalStateType = { 15 | count: number; 16 | }; 17 | let normalStore: Store; 18 | let renderedItems: any[] = []; 19 | type RootState = ReturnType; 20 | const injectNormalSelector: InjectSelector = injectSelector; 21 | 22 | beforeEach(() => { 23 | normalStore = createStore( 24 | ({ count }: NormalStateType = { count: -1 }): NormalStateType => ({ 25 | count: count + 1, 26 | }), 27 | ); 28 | renderedItems = []; 29 | }); 30 | 31 | describe('injectSelector core subscription behavior', () => { 32 | it('selects the state on initial render', async () => { 33 | @Component({ 34 | selector: 'app-root', 35 | standalone: true, 36 | template: '
Count: {{count()}}
', 37 | }) 38 | class Testing { 39 | count = injectNormalSelector((state) => state.count); 40 | } 41 | 42 | const { getByText } = await render(Testing, { 43 | providers: [provideRedux({ store: normalStore })], 44 | }); 45 | 46 | expect(getByText('Count: 0')).toBeInTheDocument(); 47 | }); 48 | 49 | it('selects the state and renders the component when the store updates', async () => { 50 | const selector = jest.fn((s: NormalStateType) => s.count); 51 | 52 | @Component({ 53 | selector: 'app-root', 54 | standalone: true, 55 | template: '
Count: {{count()}}
', 56 | }) 57 | class Testing { 58 | count = injectNormalSelector(selector); 59 | } 60 | 61 | const { findByText } = await render(Testing, { 62 | providers: [provideRedux({ store: normalStore })], 63 | }); 64 | 65 | expect(await findByText('Count: 0')).toBeInTheDocument(); 66 | expect(selector).toHaveBeenCalledTimes(1); 67 | 68 | normalStore.dispatch({ type: '' }); 69 | 70 | expect(await findByText('Count: 1')).toBeInTheDocument(); 71 | expect(selector).toHaveBeenCalledTimes(2); 72 | }); 73 | }); 74 | 75 | describe('injectSelector lifecycle interactions', () => { 76 | it('always uses the latest state', async () => { 77 | const store = createStore((c: number = 1): number => c + 1, -1); 78 | 79 | @Component({ 80 | selector: 'app-root', 81 | standalone: true, 82 | template: '
', 83 | }) 84 | class Testing { 85 | selector = (c: number): number => c + 1; 86 | value = injectSelector(this.selector); 87 | _test = effect(() => { 88 | renderedItems.push(this.value()); 89 | }); 90 | } 91 | 92 | await render(Testing, { 93 | providers: [provideRedux({ store })], 94 | }); 95 | 96 | expect(renderedItems).toEqual([1]); 97 | 98 | store.dispatch({ type: '' }); 99 | 100 | await waitFor(() => expect(renderedItems).toEqual([1, 2])); 101 | }); 102 | 103 | it('subscribes to the store synchronously', async () => { 104 | let appSubscription: Subscription | null = null; 105 | 106 | @Component({ 107 | selector: 'child-root', 108 | standalone: true, 109 | template: '
{{count()}}
', 110 | }) 111 | class Child { 112 | count = injectNormalSelector((s) => s.count); 113 | } 114 | 115 | function injectReduxAndAssignApp() { 116 | const contextVal = inject(ReduxProvider); 117 | appSubscription = contextVal && contextVal.subscription; 118 | return contextVal; 119 | } 120 | 121 | @Component({ 122 | selector: 'app-root', 123 | imports: [Child], 124 | template: ` 125 | @if (count() === 1) { 126 | 127 | } 128 | ` 129 | }) 130 | class Parent { 131 | contextVal = injectReduxAndAssignApp(); 132 | count = injectNormalSelector((s) => s.count); 133 | } 134 | 135 | await render(Parent, { 136 | providers: [provideRedux({ store: normalStore })], 137 | }); 138 | 139 | // Parent component only 140 | expect(appSubscription!.getListeners().get().length).toBe(1); 141 | 142 | normalStore.dispatch({ type: '' }); 143 | 144 | // Parent component + 1 child component 145 | await waitFor(() => 146 | expect(appSubscription!.getListeners().get().length).toBe(2), 147 | ); 148 | }); 149 | 150 | it('unsubscribes when the component is unmounted', async () => { 151 | let appSubscription: Subscription | null = null; 152 | 153 | @Component({ 154 | selector: 'child-root', 155 | standalone: true, 156 | template: '
{{count()}}
', 157 | }) 158 | class Child { 159 | count = injectNormalSelector((s) => s.count); 160 | } 161 | 162 | function injectReduxAndAssignApp() { 163 | const contextVal = inject(ReduxProvider); 164 | appSubscription = contextVal && contextVal.subscription; 165 | return contextVal; 166 | } 167 | 168 | @Component({ 169 | selector: 'app-root', 170 | imports: [Child], 171 | template: ` 172 | @if (count() === 0) { 173 | 174 | } 175 | ` 176 | }) 177 | class Parent { 178 | contextVal = injectReduxAndAssignApp(); 179 | count = injectNormalSelector((s) => s.count); 180 | } 181 | 182 | await render(Parent, { 183 | providers: [provideRedux({ store: normalStore })], 184 | }); 185 | 186 | // Parent component only 187 | expect(appSubscription!.getListeners().get().length).toBe(2); 188 | 189 | normalStore.dispatch({ type: '' }); 190 | 191 | // Parent component + 1 child component 192 | await waitFor(() => 193 | expect(appSubscription!.getListeners().get().length).toBe(1), 194 | ); 195 | }); 196 | }); 197 | 198 | describe('performance optimizations and bail-outs', () => { 199 | it('defaults to ref-equality to prevent unnecessary updates', async () => { 200 | const state = {}; 201 | const store = createStore(() => state); 202 | 203 | @Component({ 204 | selector: 'app-root', 205 | standalone: true, 206 | template: '
', 207 | }) 208 | class Comp { 209 | value = injectSelector((s) => s); 210 | _test = effect(() => { 211 | renderedItems.push(this.value()); 212 | }); 213 | } 214 | 215 | await render(Comp, { 216 | providers: [provideRedux({ store })], 217 | }); 218 | 219 | expect(renderedItems.length).toBe(1); 220 | 221 | store.dispatch({ type: '' }); 222 | 223 | await waitFor(() => expect(renderedItems.length).toBe(1)); 224 | }); 225 | 226 | it('allows other equality functions to prevent unnecessary updates', async () => { 227 | interface StateType { 228 | count: number; 229 | stable: {}; 230 | } 231 | 232 | const store = createStore( 233 | ({ count, stable }: StateType = { count: -1, stable: {} }) => ({ 234 | count: count + 1, 235 | stable, 236 | }), 237 | ); 238 | 239 | @Component({ 240 | selector: 'app-comp', 241 | standalone: true, 242 | template: '
', 243 | }) 244 | class Comp { 245 | value = injectSelector((s: StateType) => Object.keys(s), shallowEqual); 246 | _test = effect(() => { 247 | renderedItems.push(this.value()); 248 | }); 249 | } 250 | 251 | @Component({ 252 | selector: 'app-other', 253 | standalone: true, 254 | template: '
', 255 | }) 256 | class Comp2 { 257 | value = injectSelector((s: StateType) => Object.keys(s), { 258 | equalityFn: shallowEqual, 259 | }); 260 | _test = effect(() => { 261 | renderedItems.push(this.value()); 262 | }); 263 | } 264 | 265 | @Component({ 266 | selector: 'app-root', 267 | imports: [Comp, Comp2], 268 | template: ` 269 | 270 | 271 | ` 272 | }) 273 | class App {} 274 | 275 | await render(App, { 276 | providers: [provideRedux({ store })], 277 | }); 278 | 279 | expect(renderedItems.length).toBe(2); 280 | 281 | store.dispatch({ type: '' }); 282 | 283 | await waitFor(() => expect(renderedItems.length).toBe(2)); 284 | }); 285 | 286 | it('calls selector exactly once on mount and on update', async () => { 287 | interface StateType { 288 | count: number; 289 | } 290 | 291 | const store = createStore(({ count }: StateType = { count: 0 }) => ({ 292 | count: count + 1, 293 | })); 294 | 295 | const selector = jest.fn((s: StateType) => { 296 | return s.count; 297 | }); 298 | const renderedItems: number[] = []; 299 | 300 | @Component({ 301 | selector: 'app-root', 302 | standalone: true, 303 | template: '
', 304 | }) 305 | class Comp { 306 | value = injectSelector(selector); 307 | _test = effect(() => { 308 | renderedItems.push(this.value()); 309 | }); 310 | } 311 | 312 | await render(Comp, { 313 | providers: [provideRedux({ store })], 314 | }); 315 | 316 | expect(selector).toHaveBeenCalledTimes(1); 317 | expect(renderedItems.length).toEqual(1); 318 | 319 | store.dispatch({ type: '' }); 320 | 321 | await waitFor(() => expect(selector).toHaveBeenCalledTimes(2)); 322 | expect(renderedItems.length).toEqual(1); 323 | }); 324 | }); 325 | -------------------------------------------------------------------------------- /projects/angular-redux/src/tests/injections.withTypes.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ThunkAction } from '@reduxjs/toolkit'; 2 | import { 3 | configureStore, 4 | createAsyncThunk, 5 | createSlice, 6 | } from '@reduxjs/toolkit'; 7 | import { injectDispatch, injectSelector, injectStore } from '../public-api'; 8 | 9 | export interface CounterState { 10 | counter: number; 11 | } 12 | 13 | const initialState: CounterState = { 14 | counter: 0, 15 | }; 16 | 17 | export const counterSlice = createSlice({ 18 | name: 'counter', 19 | initialState, 20 | reducers: { 21 | increment(state) { 22 | state.counter++; 23 | }, 24 | }, 25 | }); 26 | 27 | export function fetchCount(amount = 1) { 28 | return new Promise<{ data: number }>((resolve) => 29 | setTimeout(() => resolve({ data: amount }), 500), 30 | ); 31 | } 32 | 33 | export const incrementAsync = createAsyncThunk( 34 | 'counter/fetchCount', 35 | async (amount: number) => { 36 | const response = await fetchCount(amount); 37 | // The value we return becomes the `fulfilled` action payload 38 | return response.data; 39 | }, 40 | ); 41 | 42 | const { increment } = counterSlice.actions; 43 | 44 | const counterStore = configureStore({ 45 | reducer: counterSlice.reducer, 46 | }); 47 | 48 | type AppStore = typeof counterStore; 49 | type AppDispatch = typeof counterStore.dispatch; 50 | type RootState = ReturnType; 51 | type AppThunk = ThunkAction< 52 | ThunkReturnType, 53 | RootState, 54 | unknown, 55 | Action 56 | >; 57 | 58 | describe('injectSelector.withTypes()', () => { 59 | test('should return injectSelector', () => { 60 | const injectAppSelector = injectSelector.withTypes(); 61 | 62 | expect(injectAppSelector).toBe(injectSelector); 63 | }); 64 | }); 65 | 66 | describe('injectDispatch.withTypes()', () => { 67 | test('should return injectDispatch', () => { 68 | const injectAppDispatch = injectDispatch.withTypes(); 69 | 70 | expect(injectAppDispatch).toBe(injectDispatch); 71 | }); 72 | }); 73 | 74 | describe('injectStore.withTypes()', () => { 75 | test('should return injectStore', () => { 76 | const injectAppStore = injectStore.withTypes(); 77 | 78 | expect(injectAppStore).toBe(injectStore); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /projects/angular-redux/src/tests/utils/Subscription.spec.ts: -------------------------------------------------------------------------------- 1 | import { createSubscription, Subscription } from '../../lib/utils/Subscription'; 2 | import { Store } from 'redux'; 3 | 4 | describe('Subscription', () => { 5 | let notifications: string[]; 6 | let store: Store; 7 | let parent: Subscription; 8 | 9 | beforeEach(() => { 10 | notifications = []; 11 | store = { subscribe: () => jest.fn() } as unknown as Store; 12 | 13 | parent = createSubscription(store); 14 | parent.onStateChange = () => {}; 15 | parent.trySubscribe(); 16 | }); 17 | 18 | function subscribeChild(name: string) { 19 | const child = createSubscription(store, parent); 20 | child.onStateChange = () => notifications.push(name); 21 | child.trySubscribe(); 22 | return child; 23 | } 24 | 25 | it('listeners are notified in order', () => { 26 | subscribeChild('child1'); 27 | subscribeChild('child2'); 28 | subscribeChild('child3'); 29 | subscribeChild('child4'); 30 | 31 | parent.notifyNestedSubs(); 32 | 33 | expect(notifications).toEqual(['child1', 'child2', 'child3', 'child4']); 34 | }); 35 | 36 | it('listeners can be unsubscribed', () => { 37 | const child1 = subscribeChild('child1'); 38 | const child2 = subscribeChild('child2'); 39 | const child3 = subscribeChild('child3'); 40 | 41 | child2.tryUnsubscribe(); 42 | parent.notifyNestedSubs(); 43 | 44 | expect(notifications).toEqual(['child1', 'child3']); 45 | notifications.length = 0; 46 | 47 | child1.tryUnsubscribe(); 48 | parent.notifyNestedSubs(); 49 | 50 | expect(notifications).toEqual(['child3']); 51 | notifications.length = 0; 52 | 53 | child3.tryUnsubscribe(); 54 | parent.notifyNestedSubs(); 55 | 56 | expect(notifications).toEqual([]); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /projects/angular-redux/src/tests/utils/shallowEqual.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from '../../public-api'; 2 | 3 | describe('Utils', () => { 4 | describe('shallowEqual', () => { 5 | it('should return true if arguments fields are equal', () => { 6 | expect( 7 | shallowEqual( 8 | { a: 1, b: 2, c: undefined }, 9 | { a: 1, b: 2, c: undefined }, 10 | ), 11 | ).toBe(true); 12 | 13 | expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe( 14 | true, 15 | ); 16 | 17 | const o = {}; 18 | expect(shallowEqual({ a: 1, b: 2, c: o }, { a: 1, b: 2, c: o })).toBe( 19 | true, 20 | ); 21 | 22 | const d = function () { 23 | return 1; 24 | }; 25 | expect( 26 | shallowEqual({ a: 1, b: 2, c: o, d }, { a: 1, b: 2, c: o, d }), 27 | ).toBe(true); 28 | }); 29 | 30 | it('should return false if arguments fields are different function identities', () => { 31 | expect( 32 | shallowEqual( 33 | { 34 | a: 1, 35 | b: 2, 36 | d: function () { 37 | return 1; 38 | }, 39 | }, 40 | { 41 | a: 1, 42 | b: 2, 43 | d: function () { 44 | return 1; 45 | }, 46 | }, 47 | ), 48 | ).toBe(false); 49 | }); 50 | 51 | it('should return false if first argument has too many keys', () => { 52 | expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false); 53 | }); 54 | 55 | it('should return false if second argument has too many keys', () => { 56 | expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false); 57 | }); 58 | 59 | it('should return false if arguments have different keys', () => { 60 | expect( 61 | shallowEqual( 62 | { a: 1, b: 2, c: undefined }, 63 | { a: 1, bb: 2, c: undefined }, 64 | ), 65 | ).toBe(false); 66 | }); 67 | 68 | it('should compare two NaN values', () => { 69 | expect(shallowEqual(NaN, NaN)).toBe(true); 70 | }); 71 | 72 | it('should compare empty objects, with false', () => { 73 | expect(shallowEqual({}, false)).toBe(false); 74 | expect(shallowEqual(false, {})).toBe(false); 75 | expect(shallowEqual([], false)).toBe(false); 76 | expect(shallowEqual(false, [])).toBe(false); 77 | }); 78 | 79 | it('should compare two zero values', () => { 80 | expect(shallowEqual(0, 0)).toBe(true); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /projects/angular-redux/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/lib", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "inlineSources": true, 10 | "types": [] 11 | }, 12 | "exclude": ["**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-redux/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.lib.json", 5 | "compilerOptions": { 6 | "declarationMap": false 7 | }, 8 | "angularCompilerOptions": { 9 | "compilationMode": "partial" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/angular-redux/tsconfig.schematics.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": ".", 5 | "stripInternal": true, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "downlevelIteration": true, 9 | "outDir": "../../dist/angular-redux", 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "lib": ["es2018", "dom"], 13 | "skipLibCheck": true, 14 | "strict": true 15 | }, 16 | "include": [ 17 | "migrations/**/*.ts", 18 | "schematics/**/*.ts", 19 | "schematics-core/**/*.ts" 20 | ], 21 | "exclude": ["**/*.spec.ts"], 22 | "angularCompilerOptions": { 23 | "skipMetadataEmit": true, 24 | "enableSummariesForJit": false, 25 | "enableIvy": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/angular-redux/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/spec", 7 | "emitDecoratorMetadata": true, 8 | "types": ["jest"] 9 | }, 10 | "include": ["**/*.spec.ts", "**/*.d.ts"], 11 | "exclude": ["./schematics/**/*.ts", "./schematics-core/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "paths": { 7 | "@reduxjs/angular-redux": ["./dist/angular-redux"] 8 | }, 9 | "outDir": "./dist/out-tsc", 10 | "strict": true, 11 | "noImplicitOverride": true, 12 | "noPropertyAccessFromIndexSignature": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "skipLibCheck": true, 16 | "isolatedModules": true, 17 | "esModuleInterop": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "bundler", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "ES2022", 25 | "lib": ["ES2022", "dom"], 26 | "types": ["jest"] 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /package.lock.json 4 | 5 | # production 6 | /build 7 | 8 | # generated files 9 | .docusaurus/ 10 | website/.docusaurus/ 11 | .cache-loader 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | This website was created with [Docusaurus](https://docusaurus.io/). 2 | 3 | # What's In This Document 4 | 5 | - [Get Started in 5 Minutes](#get-started-in-5-minutes) 6 | - [Directory Structure](#directory-structure) 7 | - [Editing Content](#editing-content) 8 | - [Adding Content](#adding-content) 9 | - [Full Documentation](#full-documentation) 10 | 11 | # Get Started in 5 Minutes 12 | 13 | 1. Make sure all the dependencies for the website are installed: 14 | 15 | ```sh 16 | # Install dependencies 17 | $ yarn 18 | ``` 19 | 20 | 2. Run your dev server: 21 | 22 | ```sh 23 | # Start the site 24 | $ yarn start 25 | ``` 26 | 27 | ## Directory Structure 28 | 29 | Your project file structure should look something like this 30 | 31 | ``` 32 | my-docusaurus/ 33 | docs/ 34 | doc-1.md 35 | doc-2.md 36 | doc-3.md 37 | website/ 38 | blog/ 39 | 2016-3-11-oldest-post.md 40 | 2017-10-24-newest-post.md 41 | core/ 42 | node_modules/ 43 | pages/ 44 | static/ 45 | css/ 46 | img/ 47 | package.json 48 | sidebar.json 49 | siteConfig.js 50 | ``` 51 | 52 | # Editing Content 53 | 54 | ## Editing an existing docs page 55 | 56 | Edit docs by navigating to `docs/` and editing the corresponding document: 57 | 58 | `docs/doc-to-be-edited.md` 59 | 60 | ```markdown 61 | --- 62 | id: page-needs-edit 63 | title: This Doc Needs To Be Edited 64 | --- 65 | 66 | Edit me... 67 | ``` 68 | 69 | For more information about docs, click [here](https://docusaurus.io/docs/en/navigation) 70 | 71 | ## Editing an existing blog post 72 | 73 | Edit blog posts by navigating to `website/blog` and editing the corresponding post: 74 | 75 | `website/blog/post-to-be-edited.md` 76 | 77 | ```markdown 78 | --- 79 | id: post-needs-edit 80 | title: This Blog Post Needs To Be Edited 81 | --- 82 | 83 | Edit me... 84 | ``` 85 | 86 | For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog) 87 | 88 | # Adding Content 89 | 90 | ## Adding a new docs page to an existing sidebar 91 | 92 | 1. Create the doc as a new markdown file in `/docs`, example `docs/newly-created-doc.md`: 93 | 94 | ```md 95 | --- 96 | id: newly-created-doc 97 | title: This Doc Needs To Be Edited 98 | --- 99 | 100 | My new content here.. 101 | ``` 102 | 103 | 1. Refer to that doc's ID in an existing sidebar in `website/sidebar.json`: 104 | 105 | ```javascript 106 | // Add newly-created-doc to the Getting Started category of docs 107 | { 108 | "docs": { 109 | "Getting Started": [ 110 | "quick-start", 111 | "newly-created-doc" // new doc here 112 | ], 113 | ... 114 | }, 115 | ... 116 | } 117 | ``` 118 | 119 | For more information about adding new docs, click [here](https://docusaurus.io/docs/en/navigation) 120 | 121 | ## Adding a new blog post 122 | 123 | 1. Make sure there is a header link to your blog in `website/siteConfig.js`: 124 | 125 | `website/siteConfig.js` 126 | 127 | ```javascript 128 | headerLinks: [ 129 | ... 130 | { blog: true, label: 'Blog' }, 131 | ... 132 | ] 133 | ``` 134 | 135 | 2. Create the blog post with the format `YYYY-MM-DD-My-Blog-Post-Title.md` in `website/blog`: 136 | 137 | `website/blog/2018-05-21-New-Blog-Post.md` 138 | 139 | ```markdown 140 | --- 141 | author: Frank Li 142 | authorURL: https://twitter.com/foobarbaz 143 | authorFBID: 503283835 144 | title: New Blog Post 145 | --- 146 | 147 | Lorem Ipsum... 148 | ``` 149 | 150 | For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog) 151 | 152 | ## Adding items to your site's top navigation bar 153 | 154 | 1. Add links to docs, custom pages or external links by editing the headerLinks field of `website/siteConfig.js`: 155 | 156 | `website/siteConfig.js` 157 | 158 | ```javascript 159 | { 160 | headerLinks: [ 161 | ... 162 | /* you can add docs */ 163 | { doc: 'my-examples', label: 'Examples' }, 164 | /* you can add custom pages */ 165 | { page: 'help', label: 'Help' }, 166 | /* you can add external links */ 167 | { href: 'https://github.com/facebook/Docusaurus', label: 'GitHub' }, 168 | ... 169 | ], 170 | ... 171 | } 172 | ``` 173 | 174 | For more information about the navigation bar, click [here](https://docusaurus.io/docs/en/navigation) 175 | 176 | ## Adding custom pages 177 | 178 | 1. Docusaurus uses React components to build pages. The components are saved as .js files in `website/pages/en`: 179 | 1. If you want your page to show up in your navigation header, you will need to update `website/siteConfig.js` to add to the `headerLinks` element: 180 | 181 | `website/siteConfig.js` 182 | 183 | ```javascript 184 | { 185 | headerLinks: [ 186 | ... 187 | { page: 'my-new-custom-page', label: 'My New Custom Page' }, 188 | ... 189 | ], 190 | ... 191 | } 192 | ``` 193 | 194 | For more information about custom pages, click [here](https://docusaurus.io/docs/en/custom-pages). 195 | 196 | # Full Documentation 197 | 198 | Full documentation can be found on the [website](https://docusaurus.io/). 199 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // See https://docusaurus.io/docs/site-config for all the possible 9 | // site configuration options. 10 | 11 | const siteConfig = { 12 | presets: [ 13 | [ 14 | "@docusaurus/preset-classic", 15 | { 16 | docs: { 17 | path: "../docs", 18 | routeBasePath: "/", 19 | sidebarPath: require.resolve("./sidebars.js"), 20 | showLastUpdateTime: true, 21 | editUrl: "https://github.com/reduxjs/angular-redux/edit/main/website", 22 | include: ["{introduction,tutorials}/*.{md,mdx}"], // no other way to exclude node_modules 23 | }, 24 | theme: { 25 | customCss: [ 26 | require.resolve("./static/css/custom.css"), 27 | require.resolve("./static/css/404.css"), 28 | require.resolve("./static/css/codeblock.css"), 29 | ], 30 | }, 31 | }, 32 | ], 33 | ], 34 | title: "Angular Redux", // Title for your website. 35 | onBrokenLinks: "throw", 36 | tagline: "Official Angular bindings for Redux", 37 | url: "https://angular-redux.js.org", // Your website URL 38 | baseUrl: "/", 39 | // Used for publishing and more 40 | projectName: "angular-redux", 41 | organizationName: "reduxjs", 42 | 43 | // For no header links in the top nav bar -> headerLinks: [], 44 | /* path to images for header/footer */ 45 | favicon: "img/favicon/favicon.ico", 46 | 47 | // Add custom scripts here that would be placed in