├── .eslintignore ├── .npmrc ├── apps ├── ngrx-ducks │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── app.component.html │ │ │ ├── counter │ │ │ │ ├── store │ │ │ │ │ ├── counter │ │ │ │ │ │ ├── counter.state.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── counter.effects.ts │ │ │ │ │ │ ├── counter-mutable.selectors.ts │ │ │ │ │ │ ├── counter-immutable.selectors.ts │ │ │ │ │ │ └── counter.store-chunk.ts │ │ │ │ │ └── counter.feature.ts │ │ │ │ ├── counter.module.ts │ │ │ │ ├── counter.component.html │ │ │ │ ├── counter.component.ts │ │ │ │ └── counter.component.scss │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── test-setup.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── styles.scss │ │ ├── index.html │ │ ├── main.ts │ │ └── polyfills.ts │ ├── tsconfig.editor.json │ ├── tsconfig.spec.json │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── project.json │ └── CHANGELOG.md └── ngrx-ducks-e2e │ ├── src │ ├── support │ │ ├── index.ts │ │ └── counter.ts │ ├── videos │ │ └── counter.cy.ts.mp4 │ └── e2e │ │ └── counter │ │ └── counter.cy.ts │ ├── cypress.config.ts │ ├── .eslintrc.json │ ├── tsconfig.json │ ├── project.json │ └── CHANGELOG.md ├── libs └── core │ ├── src │ ├── test-setup.ts │ ├── lib │ │ ├── store-chunk │ │ │ ├── index.ts │ │ │ ├── reducer-registration │ │ │ │ ├── index.ts │ │ │ │ ├── has-mutable-duck.ts │ │ │ │ ├── has-own-property.ts │ │ │ │ ├── wants-to-register-plain-reducer.ts │ │ │ │ ├── wants-register-reducer-map.ts │ │ │ │ ├── store-chunk.configuration.ts │ │ │ │ ├── retrieve-reducer.ts │ │ │ │ ├── register-reducer-in-store.ts │ │ │ │ └── has-immutable-duck.ts │ │ │ ├── connect │ │ │ │ ├── index.ts │ │ │ │ ├── ignore-properties.ts │ │ │ │ ├── infer-type-prefix-from-feature-name.ts │ │ │ │ ├── connect-selectors.ts │ │ │ │ ├── connect-use-select.ts │ │ │ │ ├── is-duck.ts │ │ │ │ ├── connect.ts │ │ │ │ └── connect-dispatchers.ts │ │ │ ├── annotation-target.ts │ │ │ ├── not-constructable-error.ts │ │ │ ├── store-chunk.types.spec.ts │ │ │ ├── reducer-registrator.service.ts │ │ │ ├── store-chunk.ts │ │ │ └── store-chunk.spec.ts │ │ ├── use-actions │ │ │ ├── index.ts │ │ │ ├── constructable.ts │ │ │ ├── action-creators.ts │ │ │ ├── action-creator-filter.ts │ │ │ ├── action-creator-candidates.ts │ │ │ ├── ngrx-ducks-use-actions-cannot-initialize-static-class-fields.error.ts │ │ │ ├── use-actions.types.spec.ts │ │ │ ├── use-actions.spec.ts │ │ │ └── use-actions.ts │ │ ├── get-mutable-reducer │ │ │ ├── index.ts │ │ │ ├── get-mutable-reducer.ts │ │ │ └── get-mutable-reducer.spec.ts │ │ ├── create-duck │ │ │ ├── types │ │ │ │ ├── dispatch-plain.ts │ │ │ │ ├── reducer-plain.ts │ │ │ │ ├── dispatch-loaded.ts │ │ │ │ ├── reducer-payload.ts │ │ │ │ ├── action-plain.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── action-loaded.ts │ │ │ │ ├── action-conditional.ts │ │ │ │ ├── index.ts │ │ │ │ ├── typed-action.ts │ │ │ │ └── dispatch-definition.ts │ │ │ ├── ducks-identifier.ts │ │ │ ├── index.ts │ │ │ ├── duck-identifier-property-key.ts │ │ │ ├── dispatch.ts │ │ │ ├── ngrx-ducks-not-connected.error.ts │ │ │ ├── create-ducksified-action.ts │ │ │ ├── create-duck.ts │ │ │ └── create-duck.spec.ts │ │ ├── create-mutable-duck │ │ │ ├── types │ │ │ │ ├── index.ts │ │ │ │ ├── mutable-reducer-plain.ts │ │ │ │ ├── mutable-reducer-payload.ts │ │ │ │ └── mutable-reducer.ts │ │ │ ├── index.ts │ │ │ ├── create-mutable-duck.spec.ts │ │ │ └── create-mutable-duck.ts │ │ ├── get-reducer │ │ │ ├── index.ts │ │ │ ├── get-reducer.ts │ │ │ ├── resolve-reducers.ts │ │ │ └── get-reducer.spec.ts │ │ ├── use-select │ │ │ ├── index.ts │ │ │ ├── use-select-container.ts │ │ │ ├── use-select.ts │ │ │ ├── connect-use-select-to-store.ts │ │ │ ├── use-select.spec.ts │ │ │ ├── use-selector-factory.ts │ │ │ └── use-select-factory.spec.ts │ │ ├── use-selectors │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ ├── memoized-selector-factory.ts │ │ │ │ ├── memoized-selector-dictionary.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── selector-conditional.ts │ │ │ ├── selector-identifier-property-key.ts │ │ │ ├── use-selectors.ts │ │ │ ├── use-selectors.types.spec.ts │ │ │ ├── connect-selectors-to-store.ts │ │ │ └── use-selectors.spec.ts │ │ └── utils.ts │ └── index.ts │ ├── ng-package.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.prod.json │ ├── package.json │ ├── tsconfig.lib.json │ ├── tsconfig.json │ ├── README.md │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── project.json │ └── CHANGELOG.md ├── commitlint.config.js ├── .husky └── commit-msg ├── tools ├── build-schematics │ ├── lib │ │ ├── index.ts │ │ ├── clean.ts │ │ ├── npm-run.ts │ │ └── copy.ts │ ├── tsconfig.json │ └── index.ts └── tsconfig.tools.json ├── .prettierrc ├── jest.config.ts ├── .editorconfig ├── project.json ├── tsconfig.base.json ├── jest.preset.js ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .eslintrc.json ├── .vscode └── settings.json ├── README.md ├── nx.json ├── package.json ├── .gitignore ├── decorate-angular-cli.js └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /libs/core/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | export { Counter } from './counter'; 2 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/index.ts: -------------------------------------------------------------------------------- 1 | export { StoreChunk } from './store-chunk'; 2 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/index.ts: -------------------------------------------------------------------------------- 1 | export { useActions } from './use-actions'; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-mutable-reducer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-mutable-reducer'; 2 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/dispatch-plain.ts: -------------------------------------------------------------------------------- 1 | export type DispatchPlain = { dispatch(): void }; 2 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/types/index.ts: -------------------------------------------------------------------------------- 1 | export { MutableReducer } from './mutable-reducer'; 2 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/constructable.ts: -------------------------------------------------------------------------------- 1 | export type Constructable = new (...args: any) => any; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/index.ts: -------------------------------------------------------------------------------- 1 | export { createMutableDuck } from './create-mutable-duck'; 2 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co-IT/ngrx-ducks/HEAD/apps/ngrx-ducks/src/favicon.ico -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/reducer-plain.ts: -------------------------------------------------------------------------------- 1 | export type ReducerPlain = (slice: TSlice) => TSlice; 2 | -------------------------------------------------------------------------------- /tools/build-schematics/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clean'; 2 | export * from './copy'; 3 | export * from './npm-run'; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 80, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nx/jest'); 2 | 3 | export default { 4 | projects: getJestProjects() 5 | }; 6 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/types/mutable-reducer-plain.ts: -------------------------------------------------------------------------------- 1 | export type MutableReducerPlain = (slice: TSlice) => void; 2 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/src/videos/counter.cy.ts.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co-IT/ngrx-ducks/HEAD/apps/ngrx-ducks-e2e/src/videos/counter.cy.ts.mp4 -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/styles.scss: -------------------------------------------------------------------------------- 1 | * { 2 | background-color: #505160; 3 | font-family: 'Helvetica', Tahoma, Geneva, Verdana, sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/dispatch-loaded.ts: -------------------------------------------------------------------------------- 1 | export type DispatchLoaded = { 2 | dispatch(payload: TPayload): void; 3 | }; 4 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-reducer/index.ts: -------------------------------------------------------------------------------- 1 | export { getReducer } from './get-reducer'; 2 | export { resolveReducers } from './resolve-reducers'; 3 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter/counter.state.ts: -------------------------------------------------------------------------------- 1 | export interface CounterState { 2 | count: number; 3 | isLoading: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/index.ts: -------------------------------------------------------------------------------- 1 | export * from './register-reducer-in-store'; 2 | export * from './store-chunk.configuration'; 3 | -------------------------------------------------------------------------------- /tools/build-schematics/lib/clean.ts: -------------------------------------------------------------------------------- 1 | import { remove } from 'fs-extra'; 2 | 3 | export function clean(path: string) { 4 | return remove(path); 5 | } 6 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/index.ts: -------------------------------------------------------------------------------- 1 | export { connectUseSelectToStore } from './connect-use-select-to-store'; 2 | export { useSelect } from './use-select'; 3 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connect'; 2 | export * from './is-duck'; 3 | export * from './infer-type-prefix-from-feature-name'; 4 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/index.ts: -------------------------------------------------------------------------------- 1 | export { useSelectors } from './use-selectors'; 2 | export { connectSelectorsToStore } from './connect-selectors-to-store'; 3 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/reducer-payload.ts: -------------------------------------------------------------------------------- 1 | export type ReducerPayload = ( 2 | slice: TSlice, 3 | payload: TPayload 4 | ) => TSlice; 5 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/annotation-target.ts: -------------------------------------------------------------------------------- 1 | export interface AnnotationTarget { 2 | new (): InstanceType; 3 | ɵfac?: Function; 4 | ɵprov?: any; 5 | } 6 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/not-constructable-error.ts: -------------------------------------------------------------------------------- 1 | export function notConstructableError() { 2 | throw new Error('ɵfac: Cannot create class directly.'); 3 | } 4 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/types/mutable-reducer-payload.ts: -------------------------------------------------------------------------------- 1 | export type MutableReducerPayload = ( 2 | slice: TSlice, 3 | payload: TPayload 4 | ) => void; 5 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/ducks-identifier.ts: -------------------------------------------------------------------------------- 1 | export enum DucksIdentifier { 2 | Duck, 3 | DuckMutable, 4 | DuckDispatcherPlain, 5 | DuckDispatcherPayload, 6 | DuckPickFunction 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /libs/core/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/core", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | }, 7 | "exclude": ["jest.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './counter-immutable.selectors'; 2 | export * from './counter.effects'; 3 | export * from './counter.state'; 4 | export * from './counter.store-chunk'; 5 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/types/memoized-selector-factory.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelector } from '@ngrx/store'; 2 | 3 | export type MemoizedSelectorFactory = ( 4 | ...args: any 5 | ) => MemoizedSelector; 6 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'my-app', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/index.ts: -------------------------------------------------------------------------------- 1 | export { createDuck } from './create-duck'; 2 | export { dispatch } from './dispatch'; 3 | export { duckIdentifierPropertyKey } from './duck-identifier-property-key'; 4 | export { DucksIdentifier } from './ducks-identifier'; 5 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter.feature.ts: -------------------------------------------------------------------------------- 1 | import { CounterState } from './counter'; 2 | 3 | export const counterFeatureName = 'counter'; 4 | 5 | export interface State { 6 | counterImmutable: CounterState; 7 | counterMutable: CounterState; 8 | } 9 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/action-plain.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from '@ngrx/store'; 2 | import { TypedAction } from './typed-action'; 3 | 4 | export type ActionPlain = ActionCreator< 5 | TType, 6 | () => TypedAction 7 | >; 8 | -------------------------------------------------------------------------------- /libs/core/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const compilerOptions = () => ({ 2 | moduleResolution: 'node', 3 | target: 'es2017', 4 | baseUrl: '.', 5 | experimentalDecorators: true, 6 | paths: { 7 | '@ngrx-ducks/core': ['./libs/core/src'], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/has-mutable-duck.ts: -------------------------------------------------------------------------------- 1 | import { isMutableDuck } from '../connect'; 2 | 3 | export function hasMutableDuck(instance: any): boolean { 4 | return Object.keys(instance).some(property => 5 | isMutableDuck(instance, property) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ReducerPayload } from './reducer-payload'; 2 | import { ReducerPlain } from './reducer-plain'; 3 | 4 | export type Reducer = TPayload extends undefined 5 | ? ReducerPlain 6 | : ReducerPayload; 7 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "version": { 6 | "executor": "@jscutlery/semver:version", 7 | "options": { 8 | "syncVersions": true 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; 3 | 4 | module.exports = defineConfig({ 5 | e2e: { 6 | ...nxE2EPreset(__dirname), 7 | testIsolation: false, 8 | supportFile: false 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /libs/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tools/build-schematics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "strictNullChecks": true, 5 | "target": "es6", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "typeRoots": ["../../../node_modules/@types"] 9 | }, 10 | "include": ["./index.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/types/memoized-selector-dictionary.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelector } from '@ngrx/store'; 2 | import { MemoizedSelectorFactory } from './memoized-selector-factory'; 3 | 4 | export type MemoizedSelectorDictionary = { 5 | [key: string]: MemoizedSelector | MemoizedSelectorFactory; 6 | }; 7 | -------------------------------------------------------------------------------- /libs/core/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false, 5 | "target": "ES2022", 6 | "useDefineForClassFields": false 7 | }, 8 | "angularCompilerOptions": { 9 | "compilationMode": "partial" 10 | }, 11 | "exclude": ["jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/duck-identifier-property-key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Each duck gets an identifier attached allowing to inspect 3 | * whether it is a duck or not. 4 | * 5 | * Since the property key is used in multiple parts of the code base 6 | * we introduced a constant for it. 7 | */ 8 | export const duckIdentifierPropertyKey = '__ngrx_ducks__id'; 9 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/types/mutable-reducer.ts: -------------------------------------------------------------------------------- 1 | import { MutableReducerPayload } from './mutable-reducer-payload'; 2 | import { MutableReducerPlain } from './mutable-reducer-plain'; 3 | 4 | export type MutableReducer = TPayload extends undefined 5 | ? MutableReducerPlain 6 | : MutableReducerPayload; 7 | -------------------------------------------------------------------------------- /tools/build-schematics/lib/npm-run.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | export function npmRun(script: string) { 4 | return new Promise((resolve, reject) => 5 | spawn('npm', ['run', script], { 6 | stdio: 'inherit', 7 | shell: true 8 | }).on('close', exitCode => (exitCode === 0 ? resolve() : reject())) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [], 6 | "target": "ES2022", 7 | "useDefineForClassFields": false 8 | }, 9 | "files": ["src/main.ts", "src/polyfills.ts"], 10 | "include": ["src/**/*.d.ts"], 11 | "exclude": ["jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/has-own-property.ts: -------------------------------------------------------------------------------- 1 | export function hasOwnProperty< 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | X, 4 | Y extends PropertyKey 5 | >(obj?: X, property?: Y): obj is X & Record { 6 | if (!obj || !property) return false; 7 | 8 | return Object.hasOwnProperty.call(obj, property); 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/ignore-properties.ts: -------------------------------------------------------------------------------- 1 | export function ignoreProperties( 2 | object: any, 3 | propertiesToIgnore: string[] 4 | ): string[] { 5 | if (!object || !propertiesToIgnore) { 6 | return []; 7 | } 8 | return Object.keys(object).filter(property => 9 | propertiesToIgnore.every(propertyToIgnore => propertyToIgnore !== property) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/action-loaded.ts: -------------------------------------------------------------------------------- 1 | import { FunctionWithParametersType } from '@ngrx/store'; 2 | import { TypedAction } from './typed-action'; 3 | 4 | export type ActionLoaded< 5 | TType extends string, 6 | TPayload 7 | > = FunctionWithParametersType< 8 | [TPayload], 9 | { 10 | payload: TPayload; 11 | } & TypedAction 12 | > & 13 | TypedAction; 14 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/action-creators.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorCandidates } from './action-creator-candidates'; 2 | import { ActionCreatorFilter } from './action-creator-filter'; 3 | import { Constructable } from './constructable'; 4 | 5 | export type ActionCreators = Pick< 6 | ActionCreatorCandidates>, 7 | ActionCreatorFilter> 8 | >; 9 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgrxDucks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /libs/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngrx-ducks/core", 3 | "version": "16.0.0", 4 | "peerDependencies": { 5 | "@angular/common": ">= 14.0.0", 6 | "@angular/core": ">= 14.0.0", 7 | "@ngrx/effects": ">= 14.0.0", 8 | "@ngrx/store": ">= 14.0.0", 9 | "immer": ">= 9.0.6", 10 | "rxjs": ">= 7.0.0" 11 | }, 12 | "dependencies": { 13 | "tslib": "^2.3.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/wants-to-register-plain-reducer.ts: -------------------------------------------------------------------------------- 1 | import { StoreChunkConfiguration } from './store-chunk.configuration'; 2 | import { wantsToRegisterReducerMap } from './wants-register-reducer-map'; 3 | 4 | export function wantsToRegisterPlainReducer( 5 | configuration: StoreChunkConfiguration 6 | ): boolean { 7 | return !wantsToRegisterReducerMap(configuration); 8 | } 9 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/dispatch.ts: -------------------------------------------------------------------------------- 1 | import { DucksIdentifier } from './ducks-identifier'; 2 | import { DispatchDefinition } from './types'; 3 | 4 | export function dispatch(): DispatchDefinition { 5 | const dispatcherDefinition = (payload: TPayload) => payload; 6 | dispatcherDefinition.id = DucksIdentifier.DuckDispatcherPlain; 7 | 8 | return dispatcherDefinition as any; 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/wants-register-reducer-map.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StoreChunkConfiguration, 3 | StoreChunkConfigurationWithSlice 4 | } from './store-chunk.configuration'; 5 | 6 | export function wantsToRegisterReducerMap( 7 | configuration: StoreChunkConfiguration 8 | ): configuration is StoreChunkConfigurationWithSlice { 9 | return Object.keys(configuration).includes('slice'); 10 | } 11 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/selector-identifier-property-key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bound selectors are stored in a separate internal property to backup the 3 | * original projection functions, because during Angular compilation we noticed overrides 4 | * and optimizations that broke the selector feature. 5 | * 6 | * Maybe we should check if this is still the case. 7 | */ 8 | export const selectorIdentifierPropertyKey = '__ngrx_ducks__selectors_original'; 9 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/action-creator-filter.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from '@ngrx/store'; 2 | 3 | export type ActionCreatorFilter = { 4 | [P in keyof T]: T[P] extends ActionCreator 5 | ? P 6 | : T[P] extends LiteralContainingActionCreator 7 | ? P 8 | : never; 9 | }[keyof T]; 10 | 11 | export type LiteralContainingActionCreator = { 12 | [P in keyof T]: T[P] extends ActionCreator ? T : never; 13 | }[keyof T]; 14 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["src/plugins/index.js"], 11 | "rules": { 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-undef": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "ES2022", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"], 11 | "useDefineForClassFields": false 12 | }, 13 | "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], 14 | "include": ["**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/types/selectors.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelector } from '@ngrx/store'; 2 | import { MemoizedSelectorFactory } from './memoized-selector-factory'; 3 | import { SelectorConditional } from './selector-conditional'; 4 | 5 | export type Selectors< 6 | T extends { 7 | [key: string]: 8 | | MemoizedSelector 9 | | MemoizedSelectorFactory; 10 | } 11 | > = { 12 | [K in keyof T]: SelectorConditional; 13 | }; 14 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/action-conditional.ts: -------------------------------------------------------------------------------- 1 | import { ActionLoaded } from './action-loaded'; 2 | import { ActionPlain } from './action-plain'; 3 | import { DispatchLoaded } from './dispatch-loaded'; 4 | import { DispatchPlain } from './dispatch-plain'; 5 | 6 | export type ActionConditional = [ 7 | TPayload 8 | ] extends [undefined] 9 | ? ActionPlain & DispatchPlain 10 | : ActionLoaded & DispatchLoaded; 11 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/ngrx-ducks-not-connected.error.ts: -------------------------------------------------------------------------------- 1 | export class NgRxDucksNotConnectedError extends Error { 2 | constructor(type?: string) { 3 | super( 4 | '[NgRxDucks] Please make sure using createDuck & useSelectors inside a class' + 5 | `that is decorated with @StoreChunk().` + 6 | !type 7 | ? '' 8 | : `(affected action: "${type}")` 9 | ); 10 | 11 | Object.setPrototypeOf(this, NgRxDucksNotConnectedError.prototype); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/infer-type-prefix-from-feature-name.ts: -------------------------------------------------------------------------------- 1 | import { StoreChunkConfiguration } from '../reducer-registration'; 2 | 3 | export function inferTypePrefixFromFeatureName( 4 | configuration: 5 | | Pick 6 | | undefined 7 | ) { 8 | if (!configuration?.feature || !configuration.enableActionTypePrefixing) 9 | return ''; 10 | 11 | return `[${configuration.feature.toUpperCase()}] `; 12 | } 13 | -------------------------------------------------------------------------------- /tools/build-schematics/index.ts: -------------------------------------------------------------------------------- 1 | import { clean, copy, npmRun } from './lib'; 2 | 3 | clean('./dist') 4 | .then(() => npmRun('tsc:prod')) 5 | .then(() => 6 | copy([ 7 | ['./README.md', './dist'], 8 | ['./package.json', './dist'], 9 | ['./src/collection.json', './dist'], 10 | ['./src/**/schema.json', './dist'], 11 | ['./src/**/files/**/*.ts.template', './dist'] 12 | ]) 13 | ) 14 | .catch(err => { 15 | console.log(err); 16 | return process.exit(1); 17 | }); 18 | -------------------------------------------------------------------------------- /tools/build-schematics/lib/copy.ts: -------------------------------------------------------------------------------- 1 | import { copy as cp } from 'fs-extra'; 2 | import { join, resolve } from 'path'; 3 | 4 | const globby = require('globby'); 5 | 6 | export function copy(tasks: [string, string][]) { 7 | return Promise.all(tasks.map(task => copySingle(task[0], task[1]))); 8 | } 9 | 10 | export function copySingle(from: string, dest: string) { 11 | return globby(from).then((sources: string[]) => 12 | Promise.all(sources.map(source => cp(source, resolve(dest, source)))) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/counter.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | import { CounterComponent } from './counter.component'; 5 | import { CounterEffects } from './store/counter'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, EffectsModule.forFeature([CounterEffects])], 9 | declarations: [CounterComponent], 10 | exports: [CounterComponent] 11 | }) 12 | export class CounterModule {} 13 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/src/e2e/counter/counter.cy.ts: -------------------------------------------------------------------------------- 1 | import { Counter } from '../../support'; 2 | 3 | describe('Counter', () => { 4 | describe('When the counter starts', () => { 5 | Counter.before.openCounter(); 6 | Counter.verify.ensureInitializingIndicatorIsHidden(); 7 | Counter.verify.ensureCountEquals(10); 8 | 9 | Counter.action.clickIncrement(); 10 | Counter.verify.ensureCountEquals(11); 11 | 12 | Counter.action.clickDecrement(); 13 | Counter.verify.ensureCountEquals(10); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/connect-selectors.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { connectSelectorsToStore } from '../../use-selectors'; 3 | import { selectorIdentifierPropertyKey } from '../../use-selectors/selector-identifier-property-key'; 4 | 5 | export function connectSelectors( 6 | instance: any, 7 | property: string, 8 | store: Store 9 | ): void { 10 | if (!instance[property][selectorIdentifierPropertyKey]) { 11 | return; 12 | } 13 | connectSelectorsToStore(instance[property], store); 14 | } 15 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/use-select-container.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelector, MemoizedSelectorWithProps } from '@ngrx/store'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export type UseSelectContainer = { 5 | select: SelectFunction; 6 | }; 7 | 8 | export interface SelectFunction { 9 | ( 10 | selector: MemoizedSelectorWithProps, 11 | props: TProps 12 | ): Observable; 13 | 14 | ( 15 | selector: MemoizedSelector 16 | ): Observable; 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/index.ts: -------------------------------------------------------------------------------- 1 | export { ActionConditional } from './action-conditional'; 2 | export { ActionLoaded } from './action-loaded'; 3 | export { ActionPlain } from './action-plain'; 4 | export { DispatchDefinition } from './dispatch-definition'; 5 | export { DispatchLoaded } from './dispatch-loaded'; 6 | export { DispatchPlain } from './dispatch-plain'; 7 | export { Reducer } from './reducer'; 8 | export { ReducerPayload } from './reducer-payload'; 9 | export { ReducerPlain } from './reducer-plain'; 10 | export { TypedAction } from './typed-action'; 11 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/types/selector-conditional.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelector } from '@ngrx/store'; 2 | import { Observable } from 'rxjs'; 3 | import { MemoizedSelectorFactory } from './memoized-selector-factory'; 4 | 5 | export type SelectorConditional< 6 | T extends MemoizedSelector | MemoizedSelectorFactory 7 | > = T extends MemoizedSelector 8 | ? Observable 9 | : T extends (...args: any[]) => MemoizedSelector 10 | ? (...args: Parameters) => Observable 11 | : never; 12 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/typed-action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The models below are directly copied from ngrx/platform 3 | * (see: https://github.com/ngrx/platform/blob/master/modules/store/src/models.ts) 4 | * 5 | * TypedAction does not belong to the public API. 6 | * If TypedAction is imported via deep import, Angular CLI will print a warning 7 | * about it. 8 | * 9 | */ 10 | export interface Action { 11 | type: string; 12 | } 13 | 14 | // declare to make it property-renaming safe 15 | export declare interface TypedAction extends Action { 16 | readonly type: T; 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/connect-use-select.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { duckIdentifierPropertyKey, DucksIdentifier } from '../../create-duck'; 3 | import { connectUseSelectToStore } from '../../use-select'; 4 | 5 | export function connectUseSelect( 6 | instance: any, 7 | property: string, 8 | store: Store 9 | ): void { 10 | if ( 11 | instance[property][duckIdentifierPropertyKey] !== 12 | DucksIdentifier.DuckPickFunction 13 | ) { 14 | return; 15 | } 16 | instance[property] = connectUseSelectToStore(instance[property], store); 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/store-chunk.configuration.ts: -------------------------------------------------------------------------------- 1 | export type StoreChunkConfiguration = 2 | | StoreChunkConfigurationWithPlainReducer 3 | | StoreChunkConfigurationWithSlice; 4 | 5 | export interface StoreChunkConfigurationWithPlainReducer { 6 | feature: string; 7 | defaults: TState; 8 | enableActionTypePrefixing?: boolean; 9 | } 10 | 11 | export interface StoreChunkConfigurationWithSlice { 12 | feature: string; 13 | slice: keyof TState; 14 | defaults: TState[keyof TState]; 15 | enableActionTypePrefixing?: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /libs/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createDuck } from './lib/create-duck/create-duck'; 2 | export { dispatch } from './lib/create-duck/dispatch'; 3 | export { createMutableDuck } from './lib/create-mutable-duck/create-mutable-duck'; 4 | export { getMutableReducer } from './lib/get-mutable-reducer/get-mutable-reducer'; 5 | export { getReducer } from './lib/get-reducer/get-reducer'; 6 | export { StoreChunk } from './lib/store-chunk/store-chunk'; 7 | export { useActions } from './lib/use-actions/use-actions'; 8 | export { useSelect } from './lib/use-select/use-select'; 9 | export { useSelectors } from './lib/use-selectors/use-selectors'; 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@ngrx-ducks/core": ["libs/core/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter/counter.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, createEffect, ofType } from '@ngrx/effects'; 3 | import { delay, map } from 'rxjs/operators'; 4 | import { counterStoreChunkActions } from './counter.store-chunk'; 5 | 6 | @Injectable() 7 | export class CounterEffects { 8 | setCounter = createEffect(() => 9 | this.actions$.pipe( 10 | ofType(counterStoreChunkActions.loadCount), 11 | delay(2000), 12 | map(({ payload }) => counterStoreChunkActions.override(payload)) 13 | ) 14 | ); 15 | 16 | constructor(private actions$: Actions) {} 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/action-creator-candidates.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionLoaded, 3 | ActionPlain, 4 | DispatchLoaded, 5 | DispatchPlain, 6 | TypedAction 7 | } from '../create-duck/types'; 8 | 9 | export type ActionCreatorCandidates = { 10 | [TMember in keyof TClass]: TClass[TMember] extends TypedAction & 11 | DispatchPlain 12 | ? ActionPlain 13 | : TClass[TMember] extends DispatchLoaded & 14 | TypedAction 15 | ? ActionLoaded 16 | : TClass[TMember] extends object 17 | ? ActionCreatorCandidates 18 | : never; 19 | }; 20 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/use-select.ts: -------------------------------------------------------------------------------- 1 | import { duckIdentifierPropertyKey, DucksIdentifier } from '../create-duck'; 2 | import { NgRxDucksNotConnectedError } from '../create-duck/ngrx-ducks-not-connected.error'; 3 | import { SelectFunction } from './use-select-container'; 4 | 5 | interface PickCoat { 6 | (): never; 7 | [duckIdentifierPropertyKey]?: DucksIdentifier; 8 | } 9 | 10 | export function useSelect(): SelectFunction { 11 | const pickFacade: PickCoat = () => { 12 | throw new NgRxDucksNotConnectedError(); 13 | }; 14 | 15 | pickFacade[duckIdentifierPropertyKey] = DucksIdentifier.DuckPickFunction; 16 | 17 | return pickFacade; 18 | } 19 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/types/dispatch-definition.ts: -------------------------------------------------------------------------------- 1 | import { duckIdentifierPropertyKey } from '../duck-identifier-property-key'; 2 | import { DucksIdentifier } from '../ducks-identifier'; 3 | 4 | export type DispatchDefinition = TPayload extends boolean 5 | ? (payload: boolean) => void & { 6 | [duckIdentifierPropertyKey]: DucksIdentifier.DuckDispatcherPayload; 7 | } 8 | : TPayload extends undefined 9 | ? () => void & { 10 | [duckIdentifierPropertyKey]: DucksIdentifier.DuckDispatcherPlain; 11 | } 12 | : (payload: TPayload) => void & { 13 | [duckIdentifierPropertyKey]: DucksIdentifier.DuckDispatcherPayload; 14 | }; 15 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/ngrx-ducks-use-actions-cannot-initialize-static-class-fields.error.ts: -------------------------------------------------------------------------------- 1 | export class NgRxDucksUseActionsCannotInitializeStaticFieldsError extends Error { 2 | constructor() { 3 | super( 4 | `[NgRxDucks] It seems you are using useActions to initialize a static field inside your @StoreChunk annotated class. 5 | 6 | In ES2022 the behaviour how Decorators are transpiled has changed (see: microsoft/TypeScript#51570). 7 | Currently, it is not clear if this is a Bug or a design decision. 8 | 9 | Please visit https://co-it.gitbook.io/ngrx-ducks/migrations/v15 to see how to resolve this issue, quickly. 10 | ` 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter/counter-mutable.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { State } from '../counter.feature'; 3 | 4 | const visitCounter = createFeatureSelector('counter'); 5 | 6 | export const currentCount = createSelector( 7 | visitCounter, 8 | counter => counter.counterMutable.count 9 | ); 10 | 11 | export const currentCountWithOffset = (offset: number) => 12 | createSelector( 13 | visitCounter, 14 | counter => counter.counterMutable.count + offset 15 | ); 16 | 17 | export const isLoading = createSelector( 18 | visitCounter, 19 | counter => counter.counterMutable.isLoading 20 | ); 21 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter/counter-immutable.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { State } from '../counter.feature'; 3 | 4 | const visitCounter = createFeatureSelector('counter'); 5 | 6 | export const currentCount = createSelector( 7 | visitCounter, 8 | counter => counter.counterImmutable.count 9 | ); 10 | 11 | export const currentCountWithOffset = (offset: number) => 12 | createSelector( 13 | visitCounter, 14 | counter => counter.counterImmutable.count + offset 15 | ); 16 | 17 | export const isLoading = createSelector( 18 | visitCounter, 19 | counter => counter.counterImmutable.isLoading 20 | ); 21 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true } 15 | }; 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [GregOnNet] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /libs/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ], 16 | "compilerOptions": { 17 | "forceConsistentCasingInFileNames": true, 18 | "strict": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "target": "es2020" 22 | }, 23 | "angularCompilerOptions": { 24 | "strictInjectionParameters": true, 25 | "strictInputAccessModifiers": true, 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ], 16 | "compilerOptions": { 17 | "forceConsistentCasingInFileNames": true, 18 | "strict": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "target": "es2020" 22 | }, 23 | "angularCompilerOptions": { 24 | "strictInjectionParameters": true, 25 | "strictInputAccessModifiers": true, 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/connect-use-select-to-store.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { duckIdentifierPropertyKey, DucksIdentifier } from '../create-duck'; 3 | import { SelectFunction } from './use-select-container'; 4 | import { useSelectorFactory } from './use-selector-factory'; 5 | 6 | export function connectUseSelectToStore( 7 | pick: { 8 | [duckIdentifierPropertyKey]?: DucksIdentifier.DuckPickFunction; 9 | }, 10 | store: Store 11 | ): SelectFunction { 12 | if (pick[duckIdentifierPropertyKey] !== DucksIdentifier.DuckPickFunction) { 13 | throw new Error( 14 | 'ngrx-ducks > usePick > Given candidate is not a pick function.' 15 | ); 16 | } 17 | return useSelectorFactory(store).select; 18 | } 19 | -------------------------------------------------------------------------------- /libs/core/README.md: -------------------------------------------------------------------------------- 1 | # @ngrx-ducks/core 2 | 3 | > You can learn all about NgRx Ducks by studying the [Docs](https://co-it.gitbook.io/ngrx-ducks/). 4 | 5 | ## Demo 6 | 7 | Please checkout the demo hosted on ⚡️ StackBlitz. 8 | It shows how NgRx and NgRx Ducks work together. 9 | 10 | ## Want to help? 11 | 12 | Your feedback is always welcome! 13 | 14 | You can file [issues](https://github.com/co-it/ngrx-ducks/issues) and create [pull requests](https://github.com/co-it/ngrx-ducks/pulls) on the co-IT.eu Github repository. 15 | 16 | https://stackblitz.com/edit/ngrx-ducks-latest?file=src/app/counter/store/counter/counter.store.ts 17 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/is-duck.ts: -------------------------------------------------------------------------------- 1 | import { duckIdentifierPropertyKey } from '../../create-duck'; 2 | import { DucksIdentifier } from '../../create-duck/ducks-identifier'; 3 | 4 | export function isDuck(instance: any, property: string): boolean { 5 | return ( 6 | isImmutableDuck(instance, property) || isMutableDuck(instance, property) 7 | ); 8 | } 9 | 10 | export function isImmutableDuck(instance: any, property: string): boolean { 11 | return instance[property][duckIdentifierPropertyKey] === DucksIdentifier.Duck; 12 | } 13 | 14 | export function isMutableDuck(instance: any, property: string): boolean { 15 | return ( 16 | instance[property][duckIdentifierPropertyKey] === 17 | DucksIdentifier.DuckMutable 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"], 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"], 16 | "angularCompilerOptions": { 17 | "enableI18nLegacyMessageIdFormat": false, 18 | "strictInjectionParameters": true, 19 | "strictInputAccessModifiers": true, 20 | "strictTemplates": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/connect.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { connectDispatchers } from './connect-dispatchers'; 3 | import { connectUseSelect } from './connect-use-select'; 4 | import { connectSelectors } from './connect-selectors'; 5 | import { StoreChunkConfiguration } from '../reducer-registration'; 6 | 7 | export function connect( 8 | Token: new () => InstanceType, 9 | store: Store, 10 | configuration?: StoreChunkConfiguration 11 | ) { 12 | const instance = new Token(); 13 | Object.keys(instance).forEach(property => { 14 | connectDispatchers(instance, property, store, configuration); 15 | connectSelectors(instance, property, store); 16 | connectUseSelect(instance, property, store); 17 | }); 18 | return instance; 19 | } 20 | -------------------------------------------------------------------------------- /libs/core/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'core', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: {}, 7 | coverageDirectory: '../../coverage/libs/core', 8 | transform: { 9 | '^.+.(ts|mjs|js|html)$': [ 10 | 'jest-preset-angular', 11 | { 12 | tsconfig: '/tsconfig.spec.json', 13 | stringifyContentPathRegex: '\\.(html|svg)$' 14 | } 15 | ] 16 | }, 17 | transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/serializers/no-ng-attributes', 20 | 'jest-preset-angular/build/serializers/ng-snapshot', 21 | 'jest-preset-angular/build/serializers/html-comment' 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ngrx-ducks', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: {}, 7 | coverageDirectory: '../../coverage/apps/ngrx-ducks', 8 | transform: { 9 | '^.+.(ts|mjs|js|html)$': [ 10 | 'jest-preset-angular', 11 | { 12 | tsconfig: '/tsconfig.spec.json', 13 | stringifyContentPathRegex: '\\.(html|svg)$' 14 | } 15 | ] 16 | }, 17 | transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/serializers/no-ng-attributes', 20 | 'jest-preset-angular/build/serializers/ng-snapshot', 21 | 'jest-preset-angular/build/serializers/html-comment' 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/counter.component.html: -------------------------------------------------------------------------------- 1 | Counter 2 | 3 | 4 | {{ count$ | async }} 5 | Plus 2: {{ countWithOffset$ | async }} 6 | 7 | 8 | Increment 9 | 10 | 11 | Decrement 12 | 13 | 14 | Log to the console, demonstrating a nested Duck 15 | 16 | 17 | 18 | 19 | 24 | Initializing... 25 | 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/store-chunk.types.spec.ts: -------------------------------------------------------------------------------- 1 | import { expecter } from 'ts-snippet'; 2 | import { compilerOptions } from '../utils'; 3 | import { StoreChunk } from './store-chunk'; 4 | 5 | describe(StoreChunk.name, () => { 6 | describe('When the decorator is used with generic type parameter specifies the defaults', () => { 7 | it('passes if the defaults are provided correctly', () => { 8 | const expectSnippet = expecter( 9 | code => ` 10 | import { StoreChunk } from '@ngrx-ducks/core'; 11 | ${code} 12 | `, 13 | compilerOptions() 14 | ); 15 | 16 | expectSnippet(` 17 | interface CounterState { 18 | count: number; 19 | } 20 | 21 | @StoreChunk({ feature: 'counter', defaults: { count: 0 } }) 22 | class Counter {} 23 | `).toSucceed(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | import { StoreModule } from '@ngrx/store'; 5 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 6 | import { AppComponent } from './app.component'; 7 | import { CounterModule } from './counter/counter.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | CounterModule, 13 | 14 | StoreModule.forRoot( 15 | {}, 16 | { 17 | runtimeChecks: { 18 | strictActionImmutability: true, 19 | strictStateImmutability: true 20 | } 21 | } 22 | ), 23 | StoreDevtoolsModule.instrument(), 24 | EffectsModule.forRoot([]) 25 | ], 26 | declarations: [AppComponent], 27 | bootstrap: [AppComponent], 28 | providers: [] 29 | }) 30 | export class AppModule {} 31 | -------------------------------------------------------------------------------- /libs/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngrxDucks", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngrx-ducks", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "peacock.color": "#b52e31", 3 | "workbench.colorCustomizations": { 4 | "activityBar.activeBackground": "#d04649", 5 | "activityBar.activeBorder": "#37cb34", 6 | "activityBar.background": "#d04649", 7 | "activityBar.foreground": "#e7e7e7", 8 | "activityBar.inactiveForeground": "#e7e7e799", 9 | "activityBarBadge.background": "#37cb34", 10 | "activityBarBadge.foreground": "#15202b", 11 | "sash.hoverBorder": "#d04649", 12 | "statusBar.background": "#b52e31", 13 | "statusBar.foreground": "#e7e7e7", 14 | "statusBarItem.hoverBackground": "#d04649", 15 | "statusBarItem.remoteBackground": "#b52e31", 16 | "statusBarItem.remoteForeground": "#e7e7e7", 17 | "titleBar.activeBackground": "#b52e31", 18 | "titleBar.activeForeground": "#e7e7e7", 19 | "titleBar.inactiveBackground": "#b52e3199", 20 | "titleBar.inactiveForeground": "#e7e7e799", 21 | "commandCenter.border": "#e7e7e799" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngrxDucksMono", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngrx-ducks-mono", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-ducks-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/ngrx-ducks-e2e/src", 5 | "projectType": "application", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nx/cypress:cypress", 9 | "options": { 10 | "cypressConfig": "apps/ngrx-ducks-e2e/cypress.config.ts", 11 | "devServerTarget": "ngrx-ducks:serve:development", 12 | "tsConfig": "apps/ngrx-ducks-e2e/tsconfig.json" 13 | }, 14 | "configurations": { 15 | "production": { 16 | "devServerTarget": "ngrx-ducks:serve:production" 17 | } 18 | } 19 | }, 20 | "lint": { 21 | "executor": "@nx/linter:eslint", 22 | "outputs": ["{options.outputFile}"], 23 | "options": { 24 | "lintFilePatterns": ["apps/ngrx-ducks-e2e/**/*.{js,ts}"] 25 | } 26 | } 27 | }, 28 | "tags": [], 29 | "implicitDependencies": ["ngrx-ducks"] 30 | } 31 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/use-select.spec.ts: -------------------------------------------------------------------------------- 1 | import { expecter } from 'ts-snippet'; 2 | import { NgRxDucksNotConnectedError } from '../create-duck/ngrx-ducks-not-connected.error'; 3 | import { compilerOptions } from '../utils'; 4 | import { useSelect } from './use-select'; 5 | 6 | describe(useSelect.name, () => { 7 | const expectSnippet = expecter( 8 | code => ` 9 | import { useSelect } from '@ngrx-ducks/core'; 10 | ${code} 11 | `, 12 | compilerOptions() 13 | ); 14 | 15 | describe('When a property is initialized with usePick', () => { 16 | it('becomes a SelectFunction', () => { 17 | expectSnippet(` 18 | const select = useSelect(); 19 | `).toInfer('select', 'SelectFunction'); 20 | }); 21 | 22 | it('throws if it is used without @StoreChunk', () => { 23 | const select = useSelect(); 24 | expect(() => select({} as any)).toThrowError( 25 | new NgRxDucksNotConnectedError() 26 | ); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-mutable-reducer/get-mutable-reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionReducer } from '@ngrx/store'; 2 | import { Draft, produce } from 'immer'; 3 | import { resolveReducers } from '../get-reducer'; 4 | import { AnnotationTarget } from '../store-chunk/annotation-target'; 5 | import { StoreChunkConfiguration } from '../store-chunk/reducer-registration'; 6 | import { inferTypePrefixFromFeatureName } from '../store-chunk/connect'; 7 | 8 | export function getMutableReducer( 9 | initialState: TState, 10 | Token: AnnotationTarget, 11 | configuration?: StoreChunkConfiguration 12 | ): ActionReducer { 13 | const instance = new Token(); 14 | const typePrefix = inferTypePrefixFromFeatureName(configuration); 15 | const caseReducers = resolveReducers(instance, typePrefix); 16 | 17 | const reducer = (draft: Draft, action: Action) => { 18 | caseReducers[action.type]?.(draft, (action as any).payload); 19 | }; 20 | 21 | return produce(reducer, initialState); 22 | } 23 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/create-ducksified-action.ts: -------------------------------------------------------------------------------- 1 | import { MutableReducer } from '../create-mutable-duck/types'; 2 | import { duckIdentifierPropertyKey } from './duck-identifier-property-key'; 3 | import { DucksIdentifier } from './ducks-identifier'; 4 | import { NgRxDucksNotConnectedError } from './ngrx-ducks-not-connected.error'; 5 | import { DispatchDefinition, Reducer } from './types'; 6 | 7 | export function createDucksifiedAction( 8 | type: TType, 9 | reducer: 10 | | DispatchDefinition 11 | | Reducer 12 | | MutableReducer 13 | | undefined, 14 | identifier: DucksIdentifier 15 | ) { 16 | const action: any = (payload?: TPayload) => ({ type, payload }); 17 | 18 | action[duckIdentifierPropertyKey] = identifier; 19 | action.type = type; 20 | action.reducer = reducer; 21 | action.dispatch = () => { 22 | throw new NgRxDucksNotConnectedError(type); 23 | }; 24 | return action; 25 | } 26 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-reducer/get-reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionReducer } from '@ngrx/store'; 2 | import { resolveReducers } from './resolve-reducers'; 3 | import { AnnotationTarget } from '../store-chunk/annotation-target'; 4 | import { StoreChunkConfiguration } from '../store-chunk/reducer-registration'; 5 | import { inferTypePrefixFromFeatureName } from '../store-chunk/connect'; 6 | 7 | export function getReducer( 8 | initialState: TState, 9 | Token: AnnotationTarget, 10 | configuration?: StoreChunkConfiguration 11 | ): ActionReducer { 12 | const instance = new Token(); 13 | const typePrefix = inferTypePrefixFromFeatureName(configuration); 14 | 15 | const caseReducers: { 16 | [key: string]: Function; 17 | } = resolveReducers(instance, typePrefix); 18 | 19 | return function (state: TState = initialState, action: Action) { 20 | return caseReducers[action.type] 21 | ? caseReducers[action.type](state, (action as any).payload) 22 | : state; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/use-selectors.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelector } from '@ngrx/store'; 2 | import { throwError } from 'rxjs'; 3 | import { NgRxDucksNotConnectedError } from '../create-duck/ngrx-ducks-not-connected.error'; 4 | import { selectorIdentifierPropertyKey } from './selector-identifier-property-key'; 5 | import { MemoizedSelectorFactory } from './types/memoized-selector-factory'; 6 | import { Selectors } from './types/selectors'; 7 | 8 | export function useSelectors< 9 | T extends { 10 | [key: string]: 11 | | MemoizedSelector 12 | | MemoizedSelectorFactory; 13 | } 14 | >(selectors: T): Selectors { 15 | const selectorFunctions = Object.keys(selectors).reduce( 16 | (functions, selector) => ({ 17 | ...functions, 18 | [selector]: throwError(() => new NgRxDucksNotConnectedError()) 19 | }), 20 | {} 21 | ); 22 | 23 | return { 24 | ...selectorFunctions, 25 | [selectorIdentifierPropertyKey]: selectors 26 | } as any; 27 | } 28 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/use-selector-factory.ts: -------------------------------------------------------------------------------- 1 | import { MemoizedSelectorWithProps, select, Store } from '@ngrx/store'; 2 | import { Observable } from 'rxjs'; 3 | import { UseSelectContainer } from './use-select-container'; 4 | 5 | export function useSelectorFactory(store: Store): UseSelectContainer { 6 | return { 7 | select: pick 8 | }; 9 | 10 | function pick( 11 | selector: MemoizedSelectorWithProps, 12 | props: TProps 13 | ): Observable; 14 | function pick( 15 | selector: (state: TState) => TResult 16 | ): Observable; 17 | function pick( 18 | selector: 19 | | ((state: TState) => TResult) 20 | | MemoizedSelectorWithProps, 21 | props?: TProps 22 | ): Observable { 23 | if (!props) { 24 | return store.pipe(select(selector as any)); 25 | } 26 | 27 | return store.pipe(select(selector as any, props)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/create-duck.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from '@ngrx/store'; 2 | import { createDucksifiedAction } from './create-ducksified-action'; 3 | import { DucksIdentifier } from './ducks-identifier'; 4 | import { ActionConditional, DispatchDefinition, Reducer } from './types'; 5 | 6 | export function createDuck( 7 | type: TType, 8 | dispatch?: DispatchDefinition 9 | ): ActionConditional; 10 | export function createDuck< 11 | TType extends string, 12 | TPayload = undefined, 13 | TSlice = undefined 14 | >( 15 | type: TType, 16 | reducer: Reducer 17 | ): ActionConditional; 18 | export function createDuck( 19 | type: TType, 20 | reducer?: DispatchDefinition | Reducer 21 | ): ActionCreator { 22 | return createDucksifiedAction( 23 | type, 24 | reducer, 25 | DucksIdentifier.Duck 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registrator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActionReducer, 4 | ActionReducerMap, 5 | combineReducers, 6 | ReducerManager 7 | } from '@ngrx/store'; 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export class ReducerRegistrator { 11 | constructor(private reducerManager: ReducerManager) {} 12 | 13 | register( 14 | targetFeature: string, 15 | reducer: ActionReducer | ActionReducerMap 16 | ) { 17 | const reducersOfFeature = this.reducerManager.currentReducers[ 18 | targetFeature 19 | ]; 20 | 21 | // TODO: refine if a feature has no reducers set there is no need to combineReducers 22 | const reducersOfFeatureNext = 23 | reducersOfFeature !== undefined 24 | ? combineReducers({ ...reducersOfFeature, ...reducer }) 25 | : typeof reducer === 'function' 26 | ? reducer 27 | : combineReducers({ ...reducer }); 28 | 29 | this.reducerManager.addReducer(targetFeature, reducersOfFeatureNext); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/create-mutable-duck.spec.ts: -------------------------------------------------------------------------------- 1 | import { expecter } from 'ts-snippet'; 2 | import { compilerOptions } from '../utils'; 3 | import { createMutableDuck } from './create-mutable-duck'; 4 | describe(createMutableDuck.name, () => { 5 | const expectSnippet = expecter( 6 | (code) => ` 7 | import { createMutableDuck, dispatch } from '@ngrx-ducks/core'; 8 | ${code} 9 | `, 10 | compilerOptions() 11 | ); 12 | 13 | describe('When a mutable duck is created', () => { 14 | it('can be created with a type only', () => { 15 | const duck = createMutableDuck('Hello'); 16 | 17 | expect(duck.type).toBe('Hello'); 18 | }); 19 | 20 | it('can be created with a dispatch definition', () => { 21 | expectSnippet(` 22 | const duck = createMutableDuck('Hello', dispatch()); 23 | `).toInfer( 24 | 'duck', 25 | 'FunctionWithParametersType<[number], { payload: number; } & TypedAction<"Hello">> & TypedAction<"Hello"> & DispatchLoaded' 26 | ); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/src/support/counter.ts: -------------------------------------------------------------------------------- 1 | export class CounterBefore { 2 | openCounter() { 3 | before(() => cy.visit('/')); 4 | } 5 | } 6 | 7 | export class CounterAction { 8 | clickIncrement() { 9 | it('Click increment button!', () => { 10 | cy.get('[data-test=counter-increment-button]').click(); 11 | }); 12 | } 13 | 14 | clickDecrement() { 15 | it('Click decrement button!', () => { 16 | cy.get('[data-test=counter-decrement-button]').click(); 17 | }); 18 | } 19 | } 20 | 21 | export class CounterVerifier { 22 | ensureCountEquals(count: number) { 23 | it(`Count is equal to "${count}."`, () => { 24 | cy.get('[data-test=counter-count]').contains(count); 25 | }); 26 | } 27 | 28 | ensureInitializingIndicatorIsHidden() { 29 | it('Initializing Indicator is hidden.', () => { 30 | cy.get('[data-test=counter-initializing-indicator]').should('be.hidden'); 31 | }); 32 | } 33 | } 34 | 35 | export class Counter { 36 | static before = new CounterBefore(); 37 | static action = new CounterAction(); 38 | static verify = new CounterVerifier(); 39 | } 40 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { CounterStore } from './store/counter'; 4 | 5 | @Component({ 6 | selector: 'counter', 7 | templateUrl: './counter.component.html', 8 | styleUrls: ['./counter.component.scss'] 9 | }) 10 | export class CounterComponent { 11 | count$: Observable; 12 | countWithOffset$: Observable; 13 | 14 | isLoading$: Observable; 15 | 16 | constructor(private counter: CounterStore) { 17 | this.counter.loadCount.dispatch(10); 18 | 19 | this.count$ = this.counter.select.currentCount; 20 | this.isLoading$ = this.counter.select.isLoading; 21 | 22 | this.countWithOffset$ = this.counter.select.currentCountWithOffset(2); 23 | } 24 | 25 | increment() { 26 | this.counter.increment.dispatch(1); 27 | } 28 | 29 | decrement() { 30 | this.counter.decrement.dispatch(1); 31 | } 32 | 33 | // you can nest or group ducks in your store if you like 34 | executeNestedDuck() { 35 | this.counter.math.square.dispatch(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-mutable-reducer/get-mutable-reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { createMutableDuck } from '../create-mutable-duck/create-mutable-duck'; 2 | import { getMutableReducer } from './get-mutable-reducer'; 3 | 4 | describe(getMutableReducer.name, () => { 5 | describe('When a mutable reducer is created', () => { 6 | interface CounterSlice { 7 | count: number; 8 | } 9 | 10 | class Facade { 11 | add = createMutableDuck('add', (slice: CounterSlice, payload: number) => { 12 | slice.count += payload; 13 | }); 14 | increment = createMutableDuck('increment', (slice: CounterSlice) => { 15 | slice.count += 1; 16 | }); 17 | decrement = createMutableDuck('decrement', (slice: CounterSlice) => { 18 | slice.count -= 1; 19 | }); 20 | } 21 | 22 | it('executes the case reducer correctly', () => { 23 | const facade = new Facade(); 24 | const mutableReducer = getMutableReducer({ count: 0 }, Facade); 25 | const state = mutableReducer({ count: 5 }, facade.add(5)); 26 | 27 | expect(state.count).toEqual(10); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/retrieve-reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer } from '@ngrx/store'; 2 | import { getMutableReducer } from '../../get-mutable-reducer'; 3 | import { getReducer } from '../../get-reducer'; 4 | import { AnnotationTarget } from '../annotation-target'; 5 | import { hasImmutableDuck } from './has-immutable-duck'; 6 | import { hasMutableDuck } from './has-mutable-duck'; 7 | import { StoreChunkConfiguration } from './store-chunk.configuration'; 8 | 9 | export function retrieveReducer( 10 | initialState: unknown, 11 | Token: AnnotationTarget, 12 | configuration: StoreChunkConfiguration 13 | ): ActionReducer { 14 | const instance = new Token(); 15 | 16 | if (hasImmutableDuck(instance)) { 17 | return getReducer(initialState, Token, configuration); 18 | } 19 | 20 | if (hasMutableDuck(instance)) { 21 | return getMutableReducer(initialState, Token, configuration); 22 | } 23 | 24 | throw new Error( 25 | '[ngrx-ducks] StoreChunk: At least one Duck is required to build a reducer function. ' + 26 | 'Please use either createDuck or createMutableDuck.' 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-mutable-duck/create-mutable-duck.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator } from '@ngrx/store'; 2 | import { DucksIdentifier } from '../create-duck'; 3 | import { createDucksifiedAction } from '../create-duck/create-ducksified-action'; 4 | import { ActionConditional, DispatchDefinition } from '../create-duck/types'; 5 | import { MutableReducer } from './types'; 6 | 7 | export function createMutableDuck( 8 | type: TType, 9 | dispatch?: DispatchDefinition 10 | ): ActionConditional; 11 | export function createMutableDuck< 12 | TType extends string, 13 | TPayload = undefined, 14 | TSlice = undefined 15 | >( 16 | type: TType, 17 | reducer: MutableReducer 18 | ): ActionConditional; 19 | export function createMutableDuck( 20 | type: TType, 21 | reducer?: DispatchDefinition | MutableReducer 22 | ): ActionCreator { 23 | return createDucksifiedAction( 24 | type, 25 | reducer, 26 | DucksIdentifier.DuckMutable 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /libs/core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/core/src", 6 | "prefix": "ngrx-ducks", 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/angular:package", 10 | "outputs": ["{workspaceRoot}/dist/libs/core"], 11 | "options": { 12 | "project": "libs/core/ng-package.json" 13 | }, 14 | "configurations": { 15 | "production": { 16 | "tsConfig": "libs/core/tsconfig.lib.prod.json" 17 | }, 18 | "development": { 19 | "tsConfig": "libs/core/tsconfig.lib.json" 20 | } 21 | }, 22 | "defaultConfiguration": "production" 23 | }, 24 | "test": { 25 | "executor": "@nx/jest:jest", 26 | "outputs": ["{workspaceRoot}/coverage/libs/core"], 27 | "options": { 28 | "jestConfig": "libs/core/jest.config.ts", 29 | "passWithNoTests": true 30 | } 31 | }, 32 | "lint": { 33 | "executor": "@nx/linter:eslint", 34 | "options": { 35 | "lintFilePatterns": ["libs/core/src/**/*.ts", "libs/core/src/**/*.html"] 36 | } 37 | } 38 | }, 39 | "tags": ["lib"] 40 | } 41 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/store-chunk.ts: -------------------------------------------------------------------------------- 1 | import { inject, ɵɵdefineInjectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { AnnotationTarget } from './annotation-target'; 4 | import { connect } from './connect'; 5 | import { notConstructableError } from './not-constructable-error'; 6 | import { 7 | registerReducerInStore, 8 | StoreChunkConfiguration 9 | } from './reducer-registration'; 10 | 11 | export function StoreChunk( 12 | config?: StoreChunkConfiguration 13 | ) { 14 | const configuration = mergeConfiguration(config); 15 | return function (constructor: AnnotationTarget) { 16 | constructor.ɵfac = notConstructableError; 17 | constructor.ɵprov = ɵɵdefineInjectable({ 18 | token: constructor, 19 | providedIn: 'root', 20 | factory() { 21 | if (configuration) { 22 | registerReducerInStore(configuration, constructor); 23 | } 24 | 25 | return connect(constructor, inject(Store) as Store, configuration); 26 | } 27 | }); 28 | 29 | return constructor as any; 30 | }; 31 | } 32 | 33 | function mergeConfiguration(config: StoreChunkConfiguration | undefined) { 34 | return config && config.enableActionTypePrefixing === undefined 35 | ? { ...config, enableActionTypePrefixing: true } 36 | : config; 37 | } 38 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/use-selectors.types.spec.ts: -------------------------------------------------------------------------------- 1 | import { expecter } from 'ts-snippet'; 2 | import { compilerOptions } from '../utils'; 3 | import { useSelectors } from './use-selectors'; 4 | 5 | describe(useSelectors.name, () => { 6 | const expectSnippet = expecter( 7 | code => ` 8 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 9 | import { useSelectors } from '@ngrx-ducks/core'; 10 | 11 | const feature = createFeatureSelector('counter'); 12 | ${code} 13 | `, 14 | compilerOptions() 15 | ); 16 | 17 | describe('When a selector is passed', () => { 18 | it('yields a stream', () => { 19 | expectSnippet(` 20 | const selectorPlain = createSelector(feature, state => state); 21 | const { selectorPlain:result } = useSelectors({ selectorPlain }); 22 | `).toInfer('result', 'Observable'); 23 | }); 24 | }); 25 | 26 | describe('When a selector taking a parameter is passed', () => { 27 | it('fails', () => { 28 | expectSnippet(` 29 | const some = createSelector(feature, state => state); 30 | const selectorProperty = createSelector(some, (s, p: number) => 1); 31 | 32 | const { selectorProperty: s } = useSelectors({ selectorProperty }); 33 | `).toFail(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/register-reducer-in-store.ts: -------------------------------------------------------------------------------- 1 | import { ɵɵinject } from '@angular/core'; 2 | import { AnnotationTarget } from '../annotation-target'; 3 | import { ReducerRegistrator } from '../reducer-registrator.service'; 4 | import { retrieveReducer } from './retrieve-reducer'; 5 | import { StoreChunkConfiguration } from './store-chunk.configuration'; 6 | import { wantsToRegisterReducerMap } from './wants-register-reducer-map'; 7 | import { wantsToRegisterPlainReducer } from './wants-to-register-plain-reducer'; 8 | 9 | export function registerReducerInStore( 10 | configuration: StoreChunkConfiguration, 11 | constructor: AnnotationTarget 12 | ) { 13 | const reducerRegistrator: ReducerRegistrator = ɵɵinject(ReducerRegistrator); 14 | 15 | if (wantsToRegisterPlainReducer(configuration)) { 16 | const { feature, defaults } = configuration; 17 | const reducer = retrieveReducer(defaults, constructor, configuration); 18 | 19 | reducerRegistrator.register(feature, reducer); 20 | } 21 | 22 | if (wantsToRegisterReducerMap(configuration)) { 23 | const { feature, slice, defaults } = configuration; 24 | const reducer = retrieveReducer(defaults, constructor, configuration); 25 | 26 | if (slice) { 27 | reducerRegistrator.register(feature, { [slice]: reducer }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ngrx-ducks 2 | 3 | > ⚠️ Currently, the development of further features is not planned. We also think about freezing this project due to new features Angular received (e.g. Signals). 4 | > 🤗 Please reach out, you rely on this project, so we can take your needs into account. 5 | > 🙏🏻 Thank you! 6 | 7 | Welcome to the mono-repository containing all packages related to the Ducks. 8 | 9 | | Package | Description | 10 | | ------------ | -------------------------------------- | 11 | | [core] | Improves coding experience for [ngrx]. | 12 | 13 | [core]: ./libs/core 14 | [ngrx]: https://ngrx.io/ 15 | 16 | ## Documentation 17 | 18 | You can learn all about NgRx Ducks by studying the [Docs](https://co-it.gitbook.io/ngrx-ducks/). 19 | 20 | ## Demo 21 | 22 | Please checkout the demo hosted on ⚡️ StackBlitz. 23 | It shows how NgRx and NgRx Ducks work together. 24 | 25 | ## Want to help? 26 | 27 | Your feedback is always welcome! 28 | 29 | You can file [issues](https://github.com/co-it/ngrx-ducks/issues) and create [pull requests](https://github.com/co-it/ngrx-ducks/pulls) on the co-IT.eu Github repository. 30 | 31 | https://stackblitz.com/edit/ngrx-ducks-latest?file=src/app/counter/store/counter/counter.store.ts 32 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-reducer/resolve-reducers.ts: -------------------------------------------------------------------------------- 1 | type CaseReducerByAction = { 2 | [actionType: string]: Function; 3 | }; 4 | 5 | /** 6 | * Iterates through properties of a class and bundling all case reducer functions together. 7 | * It also looks up nested case-reducers recursively. 8 | * 9 | * @param instance Facade class 10 | * @param collectedReducers properties that can contain case reducers 11 | * @param typePrefix 12 | * @returns key-value pairs of action-type and case-reducer 13 | */ 14 | export function resolveReducers( 15 | instance: any, 16 | typePrefix: string, 17 | collectedReducers: { [key: string]: Function } = {} 18 | ): CaseReducerByAction { 19 | return Object.keys(instance).reduce((reducers, property) => { 20 | if (instance[property].reducer) { 21 | return { 22 | ...reducers, 23 | [`${typePrefix}${instance[property].type}`]: instance[property].reducer 24 | }; 25 | } else if ( 26 | isNoNgRxDuckPatchInternal(instance[property]) && 27 | Object.keys(instance[property]).length > 0 28 | ) { 29 | return resolveReducers(instance[property], typePrefix, reducers); 30 | } else { 31 | return reducers; 32 | } 33 | }, collectedReducers); 34 | } 35 | 36 | function isNoNgRxDuckPatchInternal(object: any) { 37 | return Object.keys(object).every( 38 | property => !property.match(/__ngrx_ducks__/) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-select/use-select-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector, Store } from '@ngrx/store'; 2 | import { Observable, of } from 'rxjs'; 3 | import { useSelectorFactory } from './use-selector-factory'; 4 | 5 | describe(useSelectorFactory.name, () => { 6 | describe('When a plain selector is passed', () => { 7 | it('yields a stream containing the selected data', () => { 8 | const store: Store = { pipe: () => of(null) } as any; 9 | const picker = useSelectorFactory(store); 10 | const selectorFeature = createFeatureSelector('nullish'); 11 | const selectorNumber = createSelector(selectorFeature, _state => null); 12 | 13 | const selected = picker.select(selectorNumber); 14 | expect(selected).toBeInstanceOf(Observable); 15 | }); 16 | }); 17 | 18 | describe('When a selector factory is passed', () => { 19 | it('yields a stream containing the selected data', () => { 20 | const store: Store = { pipe: () => of(undefined) } as any; 21 | const picker = useSelectorFactory(store); 22 | const selectorNumber = () => 23 | createSelector( 24 | (state: any, props: any) => state.counter[props.id], 25 | (counter: any, props: any) => counter * props.multiply 26 | ); 27 | 28 | const selected = picker.select(selectorNumber(), { 29 | id: 'counter2', 30 | multiply: 2 31 | }); 32 | 33 | expect(selected).toBeInstanceOf(Observable); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/connect/connect-dispatchers.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { ignoreProperties } from './ignore-properties'; 3 | import { isDuck } from './is-duck'; 4 | import { StoreChunkConfiguration } from '../reducer-registration'; 5 | import { inferTypePrefixFromFeatureName } from './infer-type-prefix-from-feature-name'; 6 | 7 | export function connectDispatchers( 8 | instance: any, 9 | property: string, 10 | store: Store, 11 | configuration?: StoreChunkConfiguration 12 | ): void { 13 | if (isDuck(instance, property)) { 14 | const typePrefix = inferTypePrefixFromFeatureName(configuration); 15 | const type = `${typePrefix}${instance[property].type}`; 16 | 17 | instance[property] = (payload?: any) => ({ type, payload }); 18 | instance[property].type = type; 19 | instance[property].dispatch = (payload?: any) => { 20 | store.dispatch({ type, payload }); 21 | }; 22 | } else { 23 | tryResolveDuckRecursively(instance, property, store, configuration); 24 | } 25 | } 26 | 27 | function tryResolveDuckRecursively( 28 | instance: any, 29 | property: string, 30 | store: Store, 31 | configuration?: StoreChunkConfiguration 32 | ): void { 33 | const duckCandidates = ignoreProperties(instance[property], [ 34 | 'dispatch', 35 | 'type', 36 | 'reducer' 37 | ]); 38 | 39 | if (duckCandidates.length === 0) { 40 | return; 41 | } 42 | 43 | duckCandidates.forEach(duckCandidate => 44 | connectDispatchers(instance[property], duckCandidate, store, configuration) 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/reducer-registration/has-immutable-duck.ts: -------------------------------------------------------------------------------- 1 | import { duckIdentifierPropertyKey, DucksIdentifier } from '../../create-duck'; 2 | import { selectorIdentifierPropertyKey } from '../../use-selectors/selector-identifier-property-key'; 3 | import { isImmutableDuck, isMutableDuck } from '../connect'; 4 | import { hasOwnProperty } from './has-own-property'; 5 | 6 | export function hasImmutableDuck(instance: any): boolean { 7 | return Object.keys(instance) 8 | .filter(property => ignoreDispatcherAndSelector(instance[property])) 9 | .filter(property => !isMutableDuck(instance, property)) 10 | .some( 11 | property => 12 | isImmutableDuck(instance, property) || 13 | (Object.keys(instance[property]).length > 0 && 14 | hasImmutableDuck(instance[property])) 15 | ); 16 | } 17 | 18 | /** 19 | * To optimize the resolving process of nested ducks 20 | * we ignore all known building blocks of NgRx Ducks which are clearly no 21 | * duck 22 | * @param property candidate being checked for being a duck 23 | * @returns 24 | */ 25 | function ignoreDispatcherAndSelector(property: unknown): boolean { 26 | if (!property) return false; 27 | 28 | if (hasOwnProperty(property, selectorIdentifierPropertyKey)) { 29 | return false; 30 | } 31 | 32 | if (hasOwnProperty(property, duckIdentifierPropertyKey)) { 33 | return [ 34 | DucksIdentifier.DuckDispatcherPlain, 35 | DucksIdentifier.DuckDispatcherPayload, 36 | DucksIdentifier.DuckPickFunction 37 | ].every( 38 | ducksIdentifier => property[duckIdentifierPropertyKey] !== ducksIdentifier 39 | ); 40 | } 41 | 42 | return true; 43 | } 44 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/use-actions.types.spec.ts: -------------------------------------------------------------------------------- 1 | import { expecter } from 'ts-snippet'; 2 | import { compilerOptions } from '../utils'; 3 | 4 | describe('use with createEffect', () => { 5 | const expectSnippet = expecter( 6 | code => ` 7 | import { of } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | import { Actions, ofType } from '@ngrx/effects'; 10 | import { createDuck, dispatch, useActions } from '@ngrx-ducks/core'; 11 | ${code} 12 | `, 13 | compilerOptions() 14 | ); 15 | 16 | it('without payload => is compatible with ofType operator', () => { 17 | expectSnippet(` 18 | class Chunk { 19 | otherProperty = true; 20 | 21 | hello = createDuck('Hello'); 22 | bye = createDuck('Bye', dispatch()); 23 | } 24 | 25 | const actions = useActions(Chunk); 26 | 27 | const actions$ = of(actions.hello) as Actions; 28 | const result$ = actions$.pipe( 29 | ofType(actions.hello), 30 | map(() => actions.hello()) 31 | ); 32 | `).toInfer('result$', 'Observable>'); 33 | }); 34 | 35 | it('with payload => is compatible with ofType operator', () => { 36 | expectSnippet(` 37 | class Chunk { 38 | otherProperty = true; 39 | 40 | hello = createDuck('Hello'); 41 | bye = createDuck('Bye', dispatch()); 42 | } 43 | 44 | const actions = useActions(Chunk); 45 | 46 | const actions$ = of(actions.hello) as Actions; 47 | const result$ = actions$.pipe( 48 | ofType(actions.hello), 49 | map(() => actions.bye(true)) 50 | ); 51 | `).toInfer( 52 | 'result$', 53 | 'Observable<{ payload: boolean; } & TypedAction<"Bye">>' 54 | ); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "ngrx-ducks", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "cli": { 7 | "packageManager": "pnpm" 8 | }, 9 | "tasksRunnerOptions": { 10 | "default": { 11 | "runner": "nx-cloud", 12 | "options": { 13 | "cacheableOperations": ["build", "lint", "test", "e2e"], 14 | "parallel": 1, 15 | "accessToken": "YTgwNGI3YjEtOWQ4OC00ODNhLWFjM2QtNjBlMzRhZGE2ZGExfHJlYWQtd3JpdGU=" 16 | } 17 | } 18 | }, 19 | "generators": { 20 | "@nx/angular:application": { 21 | "style": "scss", 22 | "linter": "eslint", 23 | "unitTestRunner": "jest", 24 | "e2eTestRunner": "cypress" 25 | }, 26 | "@nx/angular:library": { 27 | "linter": "eslint", 28 | "unitTestRunner": "jest" 29 | }, 30 | "@nx/angular:component": { 31 | "style": "scss" 32 | } 33 | }, 34 | "defaultProject": "ngrx-ducks", 35 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 36 | "targetDefaults": { 37 | "build": { 38 | "dependsOn": ["^build"], 39 | "inputs": ["production", "^production"] 40 | }, 41 | "e2e": { 42 | "inputs": ["default", "^production"] 43 | }, 44 | "test": { 45 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 46 | }, 47 | "lint": { 48 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 49 | } 50 | }, 51 | "namedInputs": { 52 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 53 | "sharedGlobals": [], 54 | "production": [ 55 | "default", 56 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 57 | "!{projectRoot}/tsconfig.spec.json", 58 | "!{projectRoot}/jest.config.[jt]s", 59 | "!{projectRoot}/.eslintrc.json" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/store/counter/counter.store-chunk.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createMutableDuck, 3 | dispatch, 4 | StoreChunk, 5 | useActions, 6 | useSelect, 7 | useSelectors 8 | } from '@ngrx-ducks/core'; 9 | import { counterFeatureName, State } from '../counter.feature'; 10 | import * as selectors from './counter-mutable.selectors'; 11 | import { CounterState } from './counter.state'; 12 | 13 | const initialState = { 14 | count: 0, 15 | isLoading: true 16 | }; 17 | 18 | @StoreChunk({ 19 | feature: counterFeatureName, 20 | slice: 'counterMutable', 21 | defaults: initialState 22 | }) 23 | export class CounterStore { 24 | static pick = useSelect(); 25 | select = useSelectors(selectors); 26 | 27 | /** 28 | * 29 | * You can also create aliases or build selector groups 30 | * 31 | * progress = bindSelectors({ isLoading: selectors.isLoading }); 32 | * counter = bindSelectors({ count: selectors.currentCount }); 33 | * 34 | */ 35 | 36 | readonly loadCount = createMutableDuck('Load Count', dispatch()); 37 | 38 | increment = createMutableDuck( 39 | 'Increment value', 40 | (state: CounterState, payload: number) => (state.count += payload) 41 | ); 42 | 43 | decrement = createMutableDuck( 44 | 'Decrement value', 45 | (state: CounterState, payload: number) => (state.count -= payload) 46 | ); 47 | 48 | override = createMutableDuck( 49 | 'Set value', 50 | (state: CounterState, payload: number) => { 51 | state.count = payload; 52 | state.isLoading = false; 53 | } 54 | ); 55 | 56 | math = { 57 | square: createMutableDuck('Square', () => { 58 | console.log('Nested Ducks work, too.'); 59 | }) 60 | }; 61 | } 62 | 63 | export const counterStoreChunkActions = useActions(CounterStore, { 64 | prefix: counterFeatureName 65 | }); 66 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/connect-selectors-to-store.ts: -------------------------------------------------------------------------------- 1 | import { select, Store } from '@ngrx/store'; 2 | import { selectorIdentifierPropertyKey } from './selector-identifier-property-key'; 3 | import { MemoizedSelectorDictionary } from './types/memoized-selector-dictionary'; 4 | import { Selectors } from './types/selectors'; 5 | 6 | export function connectSelectorsToStore( 7 | selectors: Selectors, 8 | store: Store 9 | ): void { 10 | const selectorsOriginal: MemoizedSelectorDictionary = selectors[ 11 | selectorIdentifierPropertyKey 12 | ] as any; 13 | 14 | Object.keys(selectorsOriginal).forEach(key => { 15 | if (isSelector(selectorsOriginal[key])) { 16 | (selectors as any)[key] = store.pipe(select(selectorsOriginal[key])); 17 | return; 18 | } 19 | 20 | if (isSelectorFactory(selectorsOriginal[key])) { 21 | (selectors as any)[key] = (...args: any) => { 22 | const selectorWithParameter = (selectorsOriginal[key] as any)(...args); 23 | 24 | return store.pipe(select(selectorWithParameter)); 25 | }; 26 | } 27 | }); 28 | } 29 | 30 | /** 31 | * Checks if internal members of a selector are present. 32 | * If every member function is found we assume it is a selector. 33 | * 34 | * @param candidate maybe a selector 35 | * @returns indicator if a selector has been passed 36 | */ 37 | function isSelector(candidate: unknown): boolean { 38 | return ( 39 | Object.prototype.hasOwnProperty.call(candidate, 'projector') && 40 | Object.prototype.hasOwnProperty.call(candidate, 'release') && 41 | Object.prototype.hasOwnProperty.call(candidate, 'setResult') && 42 | Object.prototype.hasOwnProperty.call(candidate, 'clearResult') 43 | ); 44 | } 45 | 46 | function isSelectorFactory(candidate: unknown): boolean { 47 | return typeof candidate === 'function'; 48 | } 49 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/app/counter/counter.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | color: white; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | h1 { 9 | letter-spacing: 1.2px; 10 | } 11 | 12 | .counter { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | } 17 | 18 | .count { 19 | margin: 1rem; 20 | font-size: 2rem; 21 | } 22 | 23 | .counter-actions { 24 | display: flex; 25 | flex-direction: column; 26 | 27 | button { 28 | border-radius: 2px; 29 | font-size: 1rem; 30 | padding: 0.5rem; 31 | margin: 0.5rem; 32 | letter-spacing: 1.2px; 33 | color: #ffffff; 34 | background-color: #aebd38; 35 | border: 0; 36 | } 37 | } 38 | 39 | .progress { 40 | margin: 2rem; 41 | color: #aebd38; 42 | letter-spacing: 3px; 43 | -webkit-animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 44 | both; 45 | animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 46 | } 47 | 48 | /* ---------------------------------------------- 49 | * Generated by Animista on 2019-2-10 10:41:11 50 | * w: http://animista.net, t: @cssanimista 51 | * ---------------------------------------------- */ 52 | 53 | /** 54 | * ---------------------------------------- 55 | * animation slide-in-left 56 | * ---------------------------------------- 57 | */ 58 | @-webkit-keyframes slide-in-left { 59 | 0% { 60 | -webkit-transform: translateX(-1000px); 61 | transform: translateX(-1000px); 62 | opacity: 0; 63 | } 64 | 100% { 65 | -webkit-transform: translateX(0); 66 | transform: translateX(0); 67 | opacity: 1; 68 | } 69 | } 70 | @keyframes slide-in-left { 71 | 0% { 72 | -webkit-transform: translateX(-1000px); 73 | transform: translateX(-1000px); 74 | opacity: 0; 75 | } 76 | 100% { 77 | -webkit-transform: translateX(0); 78 | transform: translateX(0); 79 | opacity: 1; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | env: 9 | NX_CLOUD_DISTRIBUTED_EXECUTION: true 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event_name != 'pull_request' }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | name: Checkout [main] 18 | with: 19 | fetch-depth: 0 20 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 21 | uses: nrwl/nx-set-shas@v2 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: '16' 25 | - run: npm install 26 | - run: npx nx-cloud start-ci-run 27 | - run: npx nx affected --target=build --parallel --max-parallel=3 28 | - run: npx nx affected --target=test --parallel --max-parallel=2 29 | - run: npx nx-cloud stop-all-agents 30 | pr: 31 | runs-on: ubuntu-latest 32 | if: ${{ github.event_name == 'pull_request' }} 33 | steps: 34 | - uses: actions/checkout@v2 35 | with: 36 | ref: ${{ github.event.pull_request.head.ref }} 37 | fetch-depth: 0 38 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 39 | uses: nrwl/nx-set-shas@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: '16' 43 | - run: npm install 44 | - run: npx nx-cloud start-ci-run 45 | - run: npx nx affected --target=build --parallel --max-parallel=3 46 | - run: npx nx affected --target=test --parallel --max-parallel=2 47 | - run: npx nx-cloud stop-all-agents 48 | agents: 49 | runs-on: ubuntu-latest 50 | name: Agent 1 51 | timeout-minutes: 60 52 | strategy: 53 | matrix: 54 | agent: [ 1, 2, 3 ] 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: actions/setup-node@v1 58 | with: 59 | node-version: '16' 60 | - run: npm install 61 | - name: Start Nx Agent ${{ matrix.agent }} 62 | run: npx nx-cloud start-agent 63 | -------------------------------------------------------------------------------- /apps/ngrx-ducks-e2e/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 6 | 7 | 8 | 9 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 10 | 11 | 12 | 13 | # [15.0.0](https://github.com/co-IT/ngrx-ducks/compare/v14.0.3...v15.0.0) (2022-11-29) 14 | 15 | 16 | 17 | ## [14.0.3](https://github.com/co-IT/ngrx-ducks/compare/v14.0.2...v14.0.3) (2022-11-29) 18 | 19 | 20 | 21 | ## [14.0.2](https://github.com/co-IT/ngrx-ducks/compare/v14.0.1...v14.0.2) (2022-11-21) 22 | 23 | 24 | 25 | ## [14.0.1](https://github.com/co-IT/ngrx-ducks/compare/v14.0.0...v14.0.1) (2022-11-19) 26 | 27 | 28 | 29 | # [14.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.1.3...v14.0.0) (2022-06-11) 30 | 31 | 32 | 33 | ## [13.1.3](https://github.com/co-IT/ngrx-ducks/compare/v13.1.2...v13.1.3) (2022-03-14) 34 | 35 | 36 | 37 | ## [13.1.2](https://github.com/co-IT/ngrx-ducks/compare/v13.1.1...v13.1.2) (2022-03-14) 38 | 39 | 40 | 41 | ## [13.1.1](https://github.com/co-IT/ngrx-ducks/compare/v13.1.0...v13.1.1) (2022-02-11) 42 | 43 | 44 | 45 | # [13.1.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.2...v13.1.0) (2022-02-11) 46 | 47 | 48 | 49 | ## [13.0.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.1...v13.0.2) (2022-01-13) 50 | 51 | 52 | 53 | ## [13.0.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0...v13.0.1) (2021-12-14) 54 | 55 | 56 | 57 | # [13.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.3...v13.0.0) (2021-12-14) 58 | 59 | 60 | 61 | # [13.0.0-alpha.3](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.2...v13.0.0-alpha.3) (2021-12-14) 62 | 63 | 64 | 65 | # [13.0.0-alpha.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.1...v13.0.0-alpha.2) (2021-12-14) 66 | 67 | 68 | 69 | # [13.0.0-alpha.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.0...v13.0.0-alpha.1) (2021-12-13) 70 | 71 | 72 | 73 | # [13.0.0-alpha.0](https://github.com/co-IT/ngrx-ducks/compare/v12.4.3...v13.0.0-alpha.0) (2021-12-13) 74 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/use-actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDuck, dispatch } from '../create-duck'; 2 | import { NgRxDucksUseActionsCannotInitializeStaticFieldsError } from './ngrx-ducks-use-actions-cannot-initialize-static-class-fields.error'; 3 | import { useActions } from './use-actions'; 4 | 5 | describe(useActions.name, () => { 6 | describe('When a class contains a duck', () => { 7 | const typeHello = 'Hello'; 8 | const typeBye = 'Bye'; 9 | const typeSurprise = 'Surprise'; 10 | 11 | class Chunk { 12 | otherProperty = true; 13 | 14 | hello = createDuck(typeHello); 15 | bye = createDuck(typeBye, dispatch()); 16 | 17 | lookInside = { 18 | surprise: createDuck(typeSurprise) 19 | }; 20 | } 21 | 22 | it('extracts the action creator', () => { 23 | const actions = useActions(Chunk); 24 | expect(actions.hello.type).toBe(typeHello); 25 | }); 26 | 27 | it('provides a working action creator without payload', () => { 28 | const actions = useActions(Chunk); 29 | const hello = actions.hello(); 30 | expect(hello.type).toBe(typeHello); 31 | }); 32 | 33 | it('provides a working action creator supporting payloads', () => { 34 | const actions = useActions(Chunk); 35 | const bye = actions.bye(true); 36 | expect(bye.type).toBe(typeBye); 37 | }); 38 | 39 | it('provides a working action creator a nested duck', () => { 40 | const actions = useActions(Chunk); 41 | const surprise = actions.lookInside.surprise(); 42 | expect(surprise.type).toBe(typeSurprise); 43 | }); 44 | 45 | it('ignores each property not being duck', () => { 46 | const actions = useActions(Chunk); 47 | expect((actions as any).otherProperty).toBeUndefined(); 48 | }); 49 | }); 50 | 51 | describe('When useActions initializes a static field inside a class annotated with @StoreChunk', () => { 52 | it('throws an error, clarifying that there are issues in ES2022', () => { 53 | expect(() => useActions(null as any)).toThrowError( 54 | NgRxDucksUseActionsCannotInitializeStaticFieldsError 55 | ); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /libs/core/src/lib/get-reducer/get-reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDuck } from '../create-duck'; 2 | import { getReducer } from './get-reducer'; 3 | 4 | describe('get-reducer', () => { 5 | describe('When a class contains ducks', () => { 6 | class Facade { 7 | add = createDuck( 8 | 'add', 9 | (slice: number, payload: number) => slice + payload 10 | ); 11 | increment = createDuck('increment', (slice: number) => ++slice); 12 | decrement = createDuck('decrement', (slice: number) => --slice); 13 | } 14 | 15 | it('gets valid reducer function', () => { 16 | const reducer = getReducer(0, Facade); 17 | const facade = new Facade(); 18 | const state = reducer(0, facade.increment()); 19 | 20 | expect(state).toBe(1); 21 | }); 22 | 23 | it('recognizes all reducer functions of facade', () => { 24 | const reducer = getReducer(0, Facade); 25 | const facade = new Facade(); 26 | 27 | let state = reducer(0, facade.increment()); 28 | state = reducer(state, facade.decrement()); 29 | 30 | expect(state).toBe(0); 31 | }); 32 | 33 | it('recognizes reducer functions requiring payload', () => { 34 | const reducer = getReducer(0, Facade); 35 | const facade = new Facade(); 36 | 37 | let state = reducer(0, facade.increment()); 38 | state = reducer(state, facade.decrement()); 39 | state = reducer(state, facade.add(10)); 40 | 41 | expect(state).toBe(10); 42 | }); 43 | }); 44 | 45 | describe('When a class contains grouped ducks', () => { 46 | it('it adds them to the reducer', () => { 47 | class Facade { 48 | math = { 49 | add: createDuck( 50 | 'add', 51 | (slice: number, payload: number) => slice + payload 52 | ), 53 | increment: createDuck('increment', (slice: number) => ++slice), 54 | decrement: createDuck('decrement', (slice: number) => --slice) 55 | }; 56 | } 57 | 58 | const reducer = getReducer(0, Facade); 59 | const facade = new Facade(); 60 | 61 | const nextState = reducer(0, facade.math.increment()); 62 | expect(nextState).toBe(1); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-actions/use-actions.ts: -------------------------------------------------------------------------------- 1 | import { inferTypePrefixFromFeatureName, isDuck } from '../store-chunk/connect'; 2 | import { ActionCreators } from './action-creators'; 3 | import { Constructable } from './constructable'; 4 | import { NgRxDucksUseActionsCannotInitializeStaticFieldsError } from './ngrx-ducks-use-actions-cannot-initialize-static-class-fields.error'; 5 | 6 | interface UseActionsConfiguration { 7 | /** 8 | * Prefix being set before the action type. 9 | */ 10 | prefix: string; 11 | } 12 | 13 | export function useActions( 14 | Token: T, 15 | configuration?: UseActionsConfiguration 16 | ): ActionCreators { 17 | if (!Token) { 18 | throw new NgRxDucksUseActionsCannotInitializeStaticFieldsError(); 19 | } 20 | 21 | const instance = new Token(); 22 | const prefix = inferTypePrefixFromFeatureName({ 23 | feature: configuration?.prefix || '', 24 | enableActionTypePrefixing: !!configuration?.prefix 25 | }); 26 | 27 | const properties = Object.keys(instance); 28 | 29 | return properties.reduce( 30 | (actions, property) => 31 | aggregateActionCreators(instance, property, prefix, actions), 32 | {} 33 | ) as any; 34 | } 35 | 36 | function buildActionCreator(actionCreator: any, prefix: string) { 37 | if (!prefix) return actionCreator; 38 | 39 | const action = actionCreator(); 40 | const type = `${prefix}${action.type}`; 41 | const actionCreatorPrefixed = (payload: any) => ({ 42 | type, 43 | payload 44 | }); 45 | 46 | actionCreatorPrefixed.type = type; 47 | 48 | return actionCreatorPrefixed; 49 | } 50 | 51 | function aggregateActionCreators( 52 | instance: any, 53 | property: string, 54 | prefix: string, 55 | actionCreators: {} 56 | ): any { 57 | if (isDuck(instance, property)) { 58 | const actionCreator = buildActionCreator(instance[property], prefix); 59 | return { ...actionCreators, [property]: actionCreator }; 60 | } else if (Object.keys(instance[property]).length > 0) { 61 | return { 62 | ...actionCreators, 63 | [property]: Object.keys(instance[property]).reduce( 64 | (nestedActions, key) => 65 | aggregateActionCreators( 66 | instance[property], 67 | key, 68 | prefix, 69 | nestedActions 70 | ), 71 | {} 72 | ) 73 | }; 74 | } else { 75 | return actionCreators; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-ducks", 3 | "version": "16.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "nx", 7 | "postinstall": "node ./decorate-angular-cli.js", 8 | "start": "nx serve", 9 | "build": "nx build", 10 | "test": "nx test", 11 | "test:e2e": "nx e2e", 12 | "release:patch": "nx run workspace:version --releaseAs=patch", 13 | "release:major": "nx run workspace:version --releaseAs=major", 14 | "prepare": "husky install" 15 | }, 16 | "private": true, 17 | "dependencies": { 18 | "@angular/animations": "16.0.6", 19 | "@angular/common": "16.0.6", 20 | "@angular/compiler": "16.0.6", 21 | "@angular/core": "16.0.6", 22 | "@angular/forms": "16.0.6", 23 | "@angular/platform-browser": "16.0.6", 24 | "@angular/platform-browser-dynamic": "16.0.6", 25 | "@angular/router": "16.0.6", 26 | "@ngrx/effects": "16.0.1", 27 | "@ngrx/store": "16.0.1", 28 | "@ngrx/store-devtools": "16.0.1", 29 | "rxjs": "7.8.1", 30 | "tslib": "2.3.1", 31 | "zone.js": "0.13.1", 32 | "@nx/angular": "16.3.2" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "16.0.6", 36 | "@angular-devkit/core": "16.0.6", 37 | "@angular-devkit/schematics": "16.0.6", 38 | "@angular-eslint/eslint-plugin": "16.0.3", 39 | "@angular-eslint/eslint-plugin-template": "16.0.3", 40 | "@angular-eslint/template-parser": "16.0.3", 41 | "@angular/cli": "~16.0.0", 42 | "@angular/compiler-cli": "16.0.6", 43 | "@angular/language-service": "16.0.6", 44 | "@commitlint/cli": "17.0.2", 45 | "@commitlint/config-conventional": "17.0.2", 46 | "@jscutlery/semver": "3.0.0", 47 | "@nx/cypress": "16.3.2", 48 | "@nx/devkit": "16.3.2", 49 | "@nx/eslint-plugin": "16.3.2", 50 | "@nx/jest": "16.3.2", 51 | "@nx/linter": "16.3.2", 52 | "@nx/workspace": "16.3.2", 53 | "@schematics/angular": "16.0.6", 54 | "@types/jest": "29.4.4", 55 | "@types/node": "14.14.33", 56 | "@typescript-eslint/eslint-plugin": "5.58.0", 57 | "@typescript-eslint/parser": "5.58.0", 58 | "cypress": "12.15.0", 59 | "eslint": "8.15.0", 60 | "eslint-config-prettier": "8.1.0", 61 | "eslint-plugin-cypress": "2.13.3", 62 | "husky": "7.0.4", 63 | "immer": "9.0.14", 64 | "jest": "29.4.3", 65 | "jest-environment-jsdom": "29.4.3", 66 | "jest-preset-angular": "13.1.1", 67 | "ng-packagr": "16.0.1", 68 | "nx": "16.3.2", 69 | "nx-cloud": "16.0.5", 70 | "postcss": "8.3.9", 71 | "postcss-import": "14.1.0", 72 | "postcss-preset-env": "7.5.0", 73 | "postcss-url": "10.1.3", 74 | "prettier": "2.6.2", 75 | "ts-jest": "29.1.0", 76 | "ts-snippet": "5.0.2", 77 | "typescript": "5.0.4" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/angular,webstorm 3 | # Edit at https://www.gitignore.io/?templates=angular,webstorm 4 | 5 | ### Angular ### 6 | ## Angular ## 7 | # compiled output 8 | .angular/ 9 | dist/ 10 | tmp/ 11 | app/**/*.js 12 | app/**/*.js.map 13 | migrations.json 14 | 15 | # dependencies 16 | node_modules/ 17 | bower_components/ 18 | 19 | # IDEs and editors 20 | .idea/ 21 | 22 | # misc 23 | .sass-cache/ 24 | connect.lock/ 25 | coverage/ 26 | libpeerconnection.log/ 27 | npm-debug.log 28 | testem.log 29 | 30 | # e2e 31 | e2e/*.js 32 | e2e/*.map 33 | 34 | #System Files 35 | .DS_Store/ 36 | 37 | ### WebStorm ### 38 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 39 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 40 | 41 | # User-specific stuff 42 | .idea/**/workspace.xml 43 | .idea/**/tasks.xml 44 | .idea/**/usage.statistics.xml 45 | .idea/**/dictionaries 46 | .idea/**/shelf 47 | 48 | # Generated files 49 | .idea/**/contentModel.xml 50 | 51 | # Sensitive or high-churn files 52 | .idea/**/dataSources/ 53 | .idea/**/dataSources.ids 54 | .idea/**/dataSources.local.xml 55 | .idea/**/sqlDataSources.xml 56 | .idea/**/dynamic.xml 57 | .idea/**/uiDesigner.xml 58 | .idea/**/dbnavigator.xml 59 | 60 | # Gradle 61 | .idea/**/gradle.xml 62 | .idea/**/libraries 63 | 64 | # Gradle and Maven with auto-import 65 | # When using Gradle or Maven with auto-import, you should exclude module files, 66 | # since they will be recreated, and may cause churn. Uncomment if using 67 | # auto-import. 68 | # .idea/modules.xml 69 | # .idea/*.iml 70 | # .idea/modules 71 | # *.iml 72 | # *.ipr 73 | 74 | # CMake 75 | cmake-build-*/ 76 | 77 | # Mongo Explorer plugin 78 | .idea/**/mongoSettings.xml 79 | 80 | # File-based project format 81 | *.iws 82 | 83 | # IntelliJ 84 | out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Cursive Clojure plugin 93 | .idea/replstate.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | fabric.properties 100 | 101 | # Editor-based Rest Client 102 | .idea/httpRequests 103 | 104 | # Android studio 3.1+ serialized cache file 105 | .idea/caches/build_file_checksums.ser 106 | 107 | ### WebStorm Patch ### 108 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 109 | 110 | # *.iml 111 | # modules.xml 112 | # .idea/misc.xml 113 | # *.ipr 114 | 115 | # Sonarlint plugin 116 | .idea/**/sonarlint/ 117 | 118 | # SonarQube Plugin 119 | .idea/**/sonarIssues.xml 120 | 121 | # Markdown Navigator plugin 122 | .idea/**/markdown-navigator.xml 123 | .idea/**/markdown-navigator/ 124 | 125 | # End of https://www.gitignore.io/api/angular,webstorm 126 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | let output; 29 | try { 30 | output = require('@nx/workspace').output; 31 | } catch (e) { 32 | console.warn( 33 | 'Angular CLI could not be decorated to enable computation caching. Please ensure @nx/workspace is installed.' 34 | ); 35 | process.exit(0); 36 | } 37 | 38 | /** 39 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 40 | * invoke the Nx CLI and get the benefits of computation caching. 41 | */ 42 | function symlinkNgCLItoNxCLI() { 43 | try { 44 | const ngPath = './node_modules/.bin/ng'; 45 | const nxPath = './node_modules/.bin/nx'; 46 | if (isWindows) { 47 | /** 48 | * This is the most reliable way to create symlink-like behavior on Windows. 49 | * Such that it works in all shells and works with npx. 50 | */ 51 | ['', '.cmd', '.ps1'].forEach(ext => { 52 | if (fs.existsSync(nxPath + ext)) 53 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 54 | }); 55 | } else { 56 | // If unix-based, symlink 57 | cp.execSync(`ln -sf ./nx ${ngPath}`); 58 | } 59 | } catch (e) { 60 | output.error({ 61 | title: 62 | 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + 63 | e.message 64 | }); 65 | throw e; 66 | } 67 | } 68 | 69 | try { 70 | symlinkNgCLItoNxCLI(); 71 | require('@nrwl/cli/lib/decorate-cli').decorateCli(); 72 | output.log({ 73 | title: 'Angular CLI has been decorated to enable computation caching.' 74 | }); 75 | } catch (e) { 76 | output.error({ 77 | title: 'Decoration of the Angular CLI did not complete successfully' 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /libs/core/src/lib/use-selectors/use-selectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { createFeatureSelector, createSelector, Store } from '@ngrx/store'; 3 | import { provideMockStore } from '@ngrx/store/testing'; 4 | import { NgRxDucksNotConnectedError } from '../create-duck/ngrx-ducks-not-connected.error'; 5 | import { connectSelectorsToStore } from './connect-selectors-to-store'; 6 | import { useSelectors } from './use-selectors'; 7 | 8 | describe(useSelectors.name, () => { 9 | const feature = createFeatureSelector('counter'); 10 | 11 | describe('Selector Plain', () => { 12 | describe('When a selector stream is used', () => { 13 | it('throws an exception since @StoreChunk is needed to get it to work', done => { 14 | const selector = createSelector(feature, state => state); 15 | 16 | const select = useSelectors({ selector }); 17 | 18 | select.selector.subscribe({ 19 | error: err => { 20 | expect(err).toBeInstanceOf(NgRxDucksNotConnectedError); 21 | done(); 22 | } 23 | }); 24 | }); 25 | }); 26 | 27 | describe('When the Store is connected', () => { 28 | it('provides data in a stream', done => { 29 | TestBed.resetTestingModule(); 30 | TestBed.configureTestingModule({ 31 | providers: [ 32 | provideMockStore({ initialState: { counter: { count: 10 } } }) 33 | ] 34 | }); 35 | 36 | const store: Store = TestBed.get(Store); 37 | const feature = createFeatureSelector<{ count: number }>('counter'); 38 | const selectorCount = createSelector(feature, counter => counter.count); 39 | 40 | const selectors = useSelectors({ selectorCount }); 41 | connectSelectorsToStore(selectors, store); 42 | 43 | selectors.selectorCount.subscribe(count => { 44 | expect(count).toBe(10); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('Selector Factory', () => { 52 | describe('When a selector gets parameters passed from a factory', () => { 53 | it('builds a function taking the parameter and providing a stream', done => { 54 | TestBed.resetTestingModule(); 55 | TestBed.configureTestingModule({ 56 | providers: [ 57 | provideMockStore({ initialState: { counter: { count: 10 } } }) 58 | ] 59 | }); 60 | 61 | const store = TestBed.inject(Store); 62 | 63 | const countWithOffset = (offset: number, _some: string) => { 64 | return createSelector(feature, (state: any) => state.count + offset); 65 | }; 66 | 67 | const selectors = useSelectors({ countWithOffset }); 68 | connectSelectorsToStore(selectors, store); 69 | 70 | selectors.countWithOffset(10, '').subscribe({ 71 | next: count => { 72 | expect(count).toBe(20); 73 | done(); 74 | } 75 | }); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-ducks", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/ngrx-ducks/src", 6 | "prefix": "ngrx-ducks-mono", 7 | "targets": { 8 | "build": { 9 | "executor": "@angular-devkit/build-angular:browser", 10 | "outputs": ["{options.outputPath}"], 11 | "options": { 12 | "outputPath": "dist/apps/ngrx-ducks", 13 | "index": "apps/ngrx-ducks/src/index.html", 14 | "main": "apps/ngrx-ducks/src/main.ts", 15 | "polyfills": "apps/ngrx-ducks/src/polyfills.ts", 16 | "tsConfig": "apps/ngrx-ducks/tsconfig.app.json", 17 | "inlineStyleLanguage": "scss", 18 | "assets": [ 19 | "apps/ngrx-ducks/src/favicon.ico", 20 | "apps/ngrx-ducks/src/assets" 21 | ], 22 | "styles": ["apps/ngrx-ducks/src/styles.scss"], 23 | "scripts": [] 24 | }, 25 | "configurations": { 26 | "production": { 27 | "budgets": [ 28 | { 29 | "type": "initial", 30 | "maximumWarning": "500kb", 31 | "maximumError": "1mb" 32 | }, 33 | { 34 | "type": "anyComponentStyle", 35 | "maximumWarning": "2kb", 36 | "maximumError": "4kb" 37 | } 38 | ], 39 | "fileReplacements": [ 40 | { 41 | "replace": "apps/ngrx-ducks/src/environments/environment.ts", 42 | "with": "apps/ngrx-ducks/src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "outputHashing": "all" 46 | }, 47 | "development": { 48 | "buildOptimizer": false, 49 | "optimization": false, 50 | "vendorChunk": true, 51 | "extractLicenses": false, 52 | "sourceMap": true, 53 | "namedChunks": true 54 | } 55 | }, 56 | "defaultConfiguration": "production" 57 | }, 58 | "serve": { 59 | "executor": "@angular-devkit/build-angular:dev-server", 60 | "configurations": { 61 | "production": { 62 | "browserTarget": "ngrx-ducks:build:production" 63 | }, 64 | "development": { 65 | "browserTarget": "ngrx-ducks:build:development" 66 | } 67 | }, 68 | "defaultConfiguration": "development" 69 | }, 70 | "extract-i18n": { 71 | "executor": "@angular-devkit/build-angular:extract-i18n", 72 | "options": { 73 | "browserTarget": "ngrx-ducks:build" 74 | } 75 | }, 76 | "lint": { 77 | "executor": "@nx/linter:eslint", 78 | "options": { 79 | "lintFilePatterns": [ 80 | "apps/ngrx-ducks/src/**/*.ts", 81 | "apps/ngrx-ducks/src/**/*.html" 82 | ] 83 | } 84 | }, 85 | "test": { 86 | "executor": "@nx/jest:jest", 87 | "outputs": ["{workspaceRoot}/coverage/apps/ngrx-ducks"], 88 | "options": { 89 | "jestConfig": "apps/ngrx-ducks/jest.config.ts", 90 | "passWithNoTests": true 91 | } 92 | } 93 | }, 94 | "tags": [] 95 | } 96 | -------------------------------------------------------------------------------- /apps/ngrx-ducks/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 6 | 7 | 8 | 9 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 10 | 11 | 12 | 13 | # [15.0.0](https://github.com/co-IT/ngrx-ducks/compare/v14.0.3...v15.0.0) (2022-11-29) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * apply work around for useActions ([842a142](https://github.com/co-IT/ngrx-ducks/commit/842a14225895f479ba506f5ab470c62de7b20306)) 19 | 20 | 21 | ### Features 22 | 23 | * **useActions:** provide a comprehensible Error if no contructor is provided ([6a72f24](https://github.com/co-IT/ngrx-ducks/commit/6a72f24897a83f032948e4f6773be12bf28814e0)) 24 | 25 | 26 | 27 | ## [14.0.3](https://github.com/co-IT/ngrx-ducks/compare/v14.0.2...v14.0.3) (2022-11-29) 28 | 29 | 30 | 31 | ## [14.0.2](https://github.com/co-IT/ngrx-ducks/compare/v14.0.1...v14.0.2) (2022-11-21) 32 | 33 | 34 | 35 | ## [14.0.1](https://github.com/co-IT/ngrx-ducks/compare/v14.0.0...v14.0.1) (2022-11-19) 36 | 37 | 38 | 39 | # [14.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.1.3...v14.0.0) (2022-06-11) 40 | 41 | 42 | ### Features 43 | 44 | * migrate to Angular 14 ([606b5b8](https://github.com/co-IT/ngrx-ducks/commit/606b5b8287ebc7140df4604a2ebf630f50ee1a87)) 45 | * remove schematics library ([383566b](https://github.com/co-IT/ngrx-ducks/commit/383566bd3d4081a1ba491eef6cb727cc4212d5cf)) 46 | 47 | 48 | 49 | ## [13.1.3](https://github.com/co-IT/ngrx-ducks/compare/v13.1.2...v13.1.3) (2022-03-14) 50 | 51 | 52 | 53 | ## [13.1.2](https://github.com/co-IT/ngrx-ducks/compare/v13.1.1...v13.1.2) (2022-03-14) 54 | 55 | 56 | 57 | ## [13.1.1](https://github.com/co-IT/ngrx-ducks/compare/v13.1.0...v13.1.1) (2022-02-11) 58 | 59 | 60 | 61 | # [13.1.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.2...v13.1.0) (2022-02-11) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * get example app to run ([32106db](https://github.com/co-IT/ngrx-ducks/commit/32106db190d1d4ef238a0d88da76e94ed1ba1210)) 67 | 68 | 69 | ### Features 70 | 71 | * add nested duck example ([e614b18](https://github.com/co-IT/ngrx-ducks/commit/e614b18bb7f5d4ab0489e1ade0074c8939c1601c)) 72 | 73 | 74 | 75 | ## [13.0.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.1...v13.0.2) (2022-01-13) 76 | 77 | 78 | 79 | ## [13.0.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0...v13.0.1) (2021-12-14) 80 | 81 | 82 | 83 | # [13.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.3...v13.0.0) (2021-12-14) 84 | 85 | 86 | 87 | # [13.0.0-alpha.3](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.2...v13.0.0-alpha.3) (2021-12-14) 88 | 89 | 90 | 91 | # [13.0.0-alpha.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.1...v13.0.0-alpha.2) (2021-12-14) 92 | 93 | 94 | ### Features 95 | 96 | * **actions:** auto-prefix action types 🚀 ([4006ed4](https://github.com/co-IT/ngrx-ducks/commit/4006ed4e2f4b05f76335aab3bf767d25d09d3741)) 97 | 98 | 99 | 100 | # [13.0.0-alpha.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.0...v13.0.0-alpha.1) (2021-12-13) 101 | 102 | 103 | 104 | # [13.0.0-alpha.0](https://github.com/co-IT/ngrx-ducks/compare/v12.4.3...v13.0.0-alpha.0) (2021-12-13) 105 | -------------------------------------------------------------------------------- /libs/core/src/lib/create-duck/create-duck.spec.ts: -------------------------------------------------------------------------------- 1 | import { expecter } from 'ts-snippet'; 2 | import { compilerOptions } from '../utils'; 3 | import { createDuck } from './create-duck'; 4 | import { dispatch } from './dispatch'; 5 | import { NgRxDucksNotConnectedError } from './ngrx-ducks-not-connected.error'; 6 | 7 | describe('createDuck', () => { 8 | const expectSnippet = expecter( 9 | code => ` 10 | import { of } from 'rxjs'; 11 | import { map } from 'rxjs/operators'; 12 | import { Actions, ofType } from '@ngrx/effects'; 13 | import { createDuck, dispatch } from '@ngrx-ducks/core'; 14 | ${code} 15 | `, 16 | compilerOptions() 17 | ); 18 | 19 | describe('vanilla action', () => { 20 | it('provides an action creator', () => { 21 | expectSnippet(` 22 | const creator = createDuck('Hello'); 23 | `).toInfer( 24 | 'creator', 25 | '(() => TypedAction<"Hello">) & TypedAction<"Hello"> & DispatchPlain' 26 | ); 27 | }); 28 | 29 | it('provides a dispatch method', () => { 30 | expectSnippet(` 31 | const { dispatch:d } = createDuck('Hello'); 32 | `).toInfer('d', '() => void'); 33 | }); 34 | 35 | it('throws if dispatch method is called', () => { 36 | /** dispatch only works after @StoreChunk connects it with the Store */ 37 | const creator = createDuck('Hello'); 38 | 39 | expect(() => creator.dispatch()).toThrowError( 40 | new NgRxDucksNotConnectedError('Hello') 41 | ); 42 | }); 43 | 44 | it('creates an action having a type', () => { 45 | const creator = createDuck('Hello'); 46 | const action = creator(); 47 | 48 | expect(action.type).toBe('Hello'); 49 | }); 50 | 51 | it('has no payload', () => { 52 | const creator = createDuck('Hello'); 53 | const action = creator(); 54 | 55 | expect((Object.keys(action) as any).payload).toBeUndefined(); 56 | }); 57 | }); 58 | 59 | describe('action with payload', () => { 60 | it('allows declaring a payload type', () => { 61 | expectSnippet(` 62 | const creator = createDuck('Hello', dispatch()); 63 | `).toInfer( 64 | 'creator', 65 | 'FunctionWithParametersType<[number], { payload: number; } & TypedAction<"Hello">> & TypedAction<"Hello"> & DispatchLoaded' 66 | ); 67 | }); 68 | 69 | it('has payload', () => { 70 | const creator = createDuck('Hello', dispatch()); 71 | const action = creator(42); 72 | 73 | expect(action.payload).toBe(42); 74 | }); 75 | }); 76 | 77 | describe('action with case reducer', () => { 78 | const expectSnippet = expecter( 79 | code => ` 80 | import { createDuck } from '@ngrx-ducks/core'; 81 | ${code} 82 | `, 83 | compilerOptions() 84 | ); 85 | 86 | it('provides an action creator mutating state', () => { 87 | expectSnippet(` 88 | const creator = createDuck('Hello', (slice: number) => slice); 89 | `).toSucceed(); 90 | }); 91 | 92 | it('contains a hidden reducer', () => { 93 | const currentSlice = 0; 94 | const creator = createDuck('Hello', (slice: number) => slice); 95 | const nextSlice = (creator as any).reducer(currentSlice); 96 | 97 | expect(nextSlice).toBe(currentSlice); 98 | }); 99 | 100 | it('fails if reducer has more than two arguments', () => { 101 | expectSnippet(` 102 | const creator = createDuck('Hello', (slice: number, payload: number, other: number) => slice); 103 | `).toFail(); 104 | }); 105 | 106 | it('fails if the return type is not equal to the slice parameter', () => { 107 | expectSnippet(` 108 | const creator = createDuck('Hello', (slice: number, payload: number) => ''); 109 | `).toFail(); 110 | }); 111 | }); 112 | 113 | describe('use with createEffect', () => { 114 | it('without payload => is compatible with ofType operator', () => { 115 | expectSnippet(` 116 | const incoming = createDuck('Hello'); 117 | const outgoing = createDuck('Bye'); 118 | const actions$ = of(incoming) as Actions; 119 | const result$ = actions$.pipe( 120 | ofType(incoming), 121 | map(() => outgoing()) 122 | ); 123 | `).toInfer('result$', 'Observable>'); 124 | }); 125 | 126 | it('with payload => is compatible with ofType operator', () => { 127 | expectSnippet(` 128 | const incoming = createDuck('Hello', dispatch()); 129 | const outgoing = createDuck('Bye', dispatch()); 130 | const actions$ = of(incoming) as Actions; 131 | const result$ = actions$.pipe( 132 | ofType(incoming), 133 | map(({ payload }) => outgoing(payload)) 134 | ); 135 | `).toInfer( 136 | 'result$', 137 | 'Observable<{ payload: number; } & TypedAction<"Bye">>' 138 | ); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /libs/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 6 | 7 | 8 | ### Features 9 | 10 | * **core:** require rxjs 7 ([7e63f94](https://github.com/co-IT/ngrx-ducks/commit/7e63f9474c777c0b597ca38fe1d5857a49739583)) 11 | 12 | 13 | 14 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 15 | 16 | 17 | ### Features 18 | 19 | * **core:** require rxjs 7 ([7e63f94](https://github.com/co-IT/ngrx-ducks/commit/7e63f9474c777c0b597ca38fe1d5857a49739583)) 20 | 21 | 22 | 23 | # [15.0.0](https://github.com/co-IT/ngrx-ducks/compare/v14.0.3...v15.0.0) (2022-11-29) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * remove schematics from README ([7c64b67](https://github.com/co-IT/ngrx-ducks/commit/7c64b67d317379c56685e8867da617826bca981a)) 29 | 30 | 31 | ### Features 32 | 33 | * **getActions:** remove deprecated helper getActions ([e1c40df](https://github.com/co-IT/ngrx-ducks/commit/e1c40dffd41933e5897bedfa5fa4db6f61096d33)) 34 | * **getActions:** remove deprecated helper usePicks ([8225796](https://github.com/co-IT/ngrx-ducks/commit/8225796d98e7c5b4b2bacf430ae82e684870ce74)) 35 | * **StoreChunk:** remove deprecated StoreFacade-Decorator ([1ceba3b](https://github.com/co-IT/ngrx-ducks/commit/1ceba3bf397707b8214c93065021b1fd9dfaadad)) 36 | * **useActions:** provide a comprehensible Error if no contructor is provided ([6a72f24](https://github.com/co-IT/ngrx-ducks/commit/6a72f24897a83f032948e4f6773be12bf28814e0)) 37 | * **useSelectors:** remove deprecated helper bindSelectors ([354cf9a](https://github.com/co-IT/ngrx-ducks/commit/354cf9a432e5b11a46142271cf17c4f893c53309)) 38 | 39 | 40 | ### BREAKING CHANGES 41 | 42 | * **useSelectors:** bindSelectors has to be replaced with useSelectors 43 | * **getActions:** usePick has to be replaced useSelect 44 | * **getActions:** getActions has to be replaced useActions 45 | * **StoreChunk:** @StoreFacade has to be replaced with @StoreChunk 46 | 47 | 48 | 49 | ## [14.0.3](https://github.com/co-IT/ngrx-ducks/compare/v14.0.2...v14.0.3) (2022-11-29) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * **core:** correct version range of rxjs ([4737bbe](https://github.com/co-IT/ngrx-ducks/commit/4737bbe3b866004e471d97e6459bab32245de645)) 55 | 56 | 57 | 58 | ## [14.0.2](https://github.com/co-IT/ngrx-ducks/compare/v14.0.1...v14.0.2) (2022-11-21) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * correct peerDependency to Angular ([80e7acf](https://github.com/co-IT/ngrx-ducks/commit/80e7acf3020b9bb293698c25b40f968cd0b84d1f)) 64 | 65 | 66 | 67 | ## [14.0.1](https://github.com/co-IT/ngrx-ducks/compare/v14.0.0...v14.0.1) (2022-11-19) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * add rxjs as peerDependency ([ecfae1b](https://github.com/co-IT/ngrx-ducks/commit/ecfae1b5d16be3496f4e29512b9c65322b423c08)) 73 | 74 | 75 | 76 | # [14.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.1.3...v14.0.0) (2022-06-11) 77 | 78 | 79 | ### Features 80 | 81 | * define peer versions ([cf61db8](https://github.com/co-IT/ngrx-ducks/commit/cf61db87da28baa810f838fff137d1586ca388f7)) 82 | * migrate to Angular 14 ([606b5b8](https://github.com/co-IT/ngrx-ducks/commit/606b5b8287ebc7140df4604a2ebf630f50ee1a87)) 83 | * remove schematics library ([383566b](https://github.com/co-IT/ngrx-ducks/commit/383566bd3d4081a1ba491eef6cb727cc4212d5cf)) 84 | 85 | 86 | 87 | ## [13.1.3](https://github.com/co-IT/ngrx-ducks/compare/v13.1.2...v13.1.3) (2022-03-14) 88 | 89 | 90 | 91 | ## [13.1.2](https://github.com/co-IT/ngrx-ducks/compare/v13.1.1...v13.1.2) (2022-03-14) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * **store-registration:** flatten existing reducers when combining them ([bfa8a92](https://github.com/co-IT/ngrx-ducks/commit/bfa8a9244d08a715516508a67a7f9a5756dedb6e)) 97 | 98 | 99 | 100 | ## [13.1.1](https://github.com/co-IT/ngrx-ducks/compare/v13.1.0...v13.1.1) (2022-02-11) 101 | 102 | 103 | 104 | # [13.1.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.2...v13.1.0) (2022-02-11) 105 | 106 | 107 | ### Features 108 | 109 | * add helper checking for props ([5fe6157](https://github.com/co-IT/ngrx-ducks/commit/5fe61577bf5d5518b132c000f48f3a1bbd33f756)) 110 | * **recursive:** ignore duck-blocks ([6770d91](https://github.com/co-IT/ngrx-ducks/commit/6770d91471a2b3a5f84a64f1c7eeaf14fc31adac)) 111 | 112 | 113 | 114 | ## [13.0.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.1...v13.0.2) (2022-01-13) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **has-immutable-duck:** consider nested ducks ([c00b243](https://github.com/co-IT/ngrx-ducks/commit/c00b243c180d6eae8597c0f318623c512561e4c1)) 120 | 121 | 122 | 123 | ## [13.0.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0...v13.0.1) (2021-12-14) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * restore README files ([a0830f7](https://github.com/co-IT/ngrx-ducks/commit/a0830f7ac821eb01aa1b2de7b0767dd258ab3b40)) 129 | 130 | 131 | 132 | # [13.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.3...v13.0.0) (2021-12-14) 133 | 134 | 135 | 136 | # [13.0.0-alpha.3](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.2...v13.0.0-alpha.3) (2021-12-14) 137 | 138 | 139 | ### Bug Fixes 140 | 141 | * **use-actions:** add type to actionCreator ([6733305](https://github.com/co-IT/ngrx-ducks/commit/6733305571dd31928ce5fa6470abb214cf4bdf7c)) 142 | 143 | 144 | 145 | # [13.0.0-alpha.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.1...v13.0.0-alpha.2) (2021-12-14) 146 | 147 | 148 | ### Features 149 | 150 | * **action-prefixung:** ➕ possibility to disable automatic prefixing ([8501330](https://github.com/co-IT/ngrx-ducks/commit/85013303acd6d84e99ec43b8cffce0d3daafb768)) 151 | * **actions:** auto-prefix action types 🚀 ([4006ed4](https://github.com/co-IT/ngrx-ducks/commit/4006ed4e2f4b05f76335aab3bf767d25d09d3741)) 152 | 153 | 154 | 155 | # [13.0.0-alpha.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.0...v13.0.0-alpha.1) (2021-12-13) 156 | 157 | 158 | ### Features 159 | 160 | * hamronize naming by introducing StoreChunk ([613eb4e](https://github.com/co-IT/ngrx-ducks/commit/613eb4e95fc3a929c4dfc49e47803d2a32cb4775)) 161 | * hamronize naming by introducing useActions ([f7db2b7](https://github.com/co-IT/ngrx-ducks/commit/f7db2b7854df42f35807b44cfa83cba0ab8426a3)) 162 | * hamronize naming by introducing useSelect ([496c9d5](https://github.com/co-IT/ngrx-ducks/commit/496c9d50ff7be273f36bd30a10f8f1492cf96e59)) 163 | * hamronize naming by introducing useSelectors ([4cb230e](https://github.com/co-IT/ngrx-ducks/commit/4cb230eb5e91be1b544a14573ebea70fefe9c97d)) 164 | 165 | 166 | 167 | # [13.0.0-alpha.0](https://github.com/co-IT/ngrx-ducks/compare/v12.4.3...v13.0.0-alpha.0) (2021-12-13) 168 | -------------------------------------------------------------------------------- /libs/core/src/lib/store-chunk/store-chunk.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { 3 | createFeatureSelector, 4 | createSelector, 5 | Store, 6 | StoreModule 7 | } from '@ngrx/store'; 8 | import { provideMockStore } from '@ngrx/store/testing'; 9 | import { createDuck, dispatch } from '../create-duck'; 10 | import { useActions } from '../use-actions'; 11 | import { useSelect } from '../use-select'; 12 | import { useSelectors } from '../use-selectors'; 13 | import { StoreChunk } from './store-chunk'; 14 | 15 | describe(StoreChunk.name, () => { 16 | describe('When a class with ducks is annotated', () => { 17 | const feature = createFeatureSelector<{ count: number }>('counter'); 18 | const selectorCount = createSelector(feature, counter => counter.count); 19 | 20 | let store: Store; 21 | let counter: Counter; 22 | 23 | @StoreChunk() 24 | class Counter { 25 | static staticsShouldBeAllowed = 'I am static'; 26 | 27 | pick = useSelect(); 28 | select = useSelectors({ selectorCount }); 29 | 30 | increment = createDuck('Increment'); 31 | add = createDuck('Add', dispatch()); 32 | } 33 | 34 | beforeEach(() => { 35 | TestBed.resetTestingModule(); 36 | TestBed.configureTestingModule({ 37 | providers: [ 38 | provideMockStore({ initialState: { counter: { count: 10 } } }) 39 | ] 40 | }); 41 | 42 | counter = TestBed.inject(Counter); 43 | store = TestBed.inject(Store); 44 | }); 45 | 46 | it('allows dispatching an action', () => { 47 | const spyDispatch = jest.spyOn(store, 'dispatch'); 48 | counter.increment.dispatch(); 49 | expect(spyDispatch).toHaveBeenCalled(); 50 | }); 51 | 52 | it('allows dispatching an action with payload', () => { 53 | const spyDispatch = jest.spyOn(store, 'dispatch'); 54 | counter.add.dispatch(23); 55 | 56 | expect(spyDispatch).toHaveBeenCalledWith({ 57 | type: counter.add.type, 58 | payload: 23 59 | }); 60 | }); 61 | 62 | it('selects data from the store', done => { 63 | counter.select.selectorCount.subscribe(count => { 64 | expect(count).toBe(10); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('selects data with pick from the store', done => { 70 | counter.pick(selectorCount).subscribe(count => { 71 | expect(count).toBe(10); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('allows using statics', () => { 77 | expect(Counter.staticsShouldBeAllowed).toBe('I am static'); 78 | }); 79 | }); 80 | 81 | describe('When a facade contains nested ducks', () => { 82 | let store: Store; 83 | let counter: Counter; 84 | 85 | @StoreChunk() 86 | class Counter { 87 | math = { 88 | increment: createDuck('Increment'), 89 | add: createDuck('Add', dispatch()) 90 | }; 91 | } 92 | 93 | beforeEach(() => { 94 | TestBed.resetTestingModule(); 95 | TestBed.configureTestingModule({ 96 | providers: [ 97 | provideMockStore({ initialState: { counter: { count: 10 } } }) 98 | ] 99 | }); 100 | 101 | counter = TestBed.inject(Counter); 102 | store = TestBed.inject(Store); 103 | }); 104 | 105 | it('resolves the duck', () => { 106 | const dispatchSpy = jest.spyOn(store, 'dispatch'); 107 | counter.math.add.dispatch(1); 108 | expect(dispatchSpy).toHaveBeenCalledWith({ type: 'Add', payload: 1 }); 109 | }); 110 | }); 111 | 112 | describe('Reducer registration', () => { 113 | describe('Reducer Function: When a facade is configured to register the reducer', () => { 114 | interface CounterState { 115 | count: number; 116 | } 117 | 118 | @StoreChunk({ feature: 'counterSlice', defaults: { count: 0 } }) 119 | class Counter { 120 | pick = useSelect(); 121 | 122 | increment = createDuck('Increment', (state: CounterState) => ({ 123 | ...state, 124 | count: state.count + 1 125 | })); 126 | } 127 | 128 | let store: Store; 129 | let counter: Counter; 130 | 131 | beforeEach(() => { 132 | TestBed.resetTestingModule(); 133 | TestBed.configureTestingModule({ imports: [StoreModule.forRoot({})] }); 134 | 135 | counter = TestBed.inject(Counter); 136 | store = TestBed.inject(Store); 137 | }); 138 | 139 | it("registers the facade's reducer in the Store", done => { 140 | counter.increment.dispatch(); 141 | 142 | store 143 | .select((state: any) => state.counterSlice.count) 144 | .subscribe(count => { 145 | expect(count).toBe(1); 146 | done(); 147 | }); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('Reducer Map: When a facade is configured to register the reducer map', () => { 153 | const counterFeatureKey = 'counterFeature'; 154 | 155 | interface CounterState { 156 | count: number; 157 | } 158 | 159 | @StoreChunk<{ counter: CounterState }>({ 160 | feature: counterFeatureKey, 161 | slice: 'counter', 162 | defaults: { count: 0 } 163 | }) 164 | class Counter { 165 | static actions = useActions(Counter, { prefix: counterFeatureKey }); 166 | pick = useSelect(); 167 | 168 | increment = createDuck('Increment', (state: CounterState) => ({ 169 | ...state, 170 | count: state.count + 1 171 | })); 172 | } 173 | 174 | let store: Store; 175 | let counter: Counter; 176 | 177 | beforeEach(() => { 178 | TestBed.resetTestingModule(); 179 | TestBed.configureTestingModule({ imports: [StoreModule.forRoot({})] }); 180 | 181 | counter = TestBed.inject(Counter); 182 | store = TestBed.inject(Store); 183 | }); 184 | 185 | it("registers the facade's reducer in the Store", done => { 186 | counter.increment.dispatch(); 187 | 188 | store 189 | .select((state: any) => state.counterFeature.counter.count) 190 | .subscribe(count => { 191 | expect(count).toBe(1); 192 | done(); 193 | }); 194 | }); 195 | 196 | it('uses the feature name as action type prefix for each duck', () => { 197 | const action = counter.increment(); 198 | expect(action.type.includes('COUNTERFEATURE')).toBe(true); 199 | }); 200 | 201 | it('uses the feature name as action type prefix for each extracted action', () => { 202 | const action = Counter.actions.increment(); 203 | 204 | expect(action.type.includes('COUNTERFEATURE')).toBe(true); 205 | }); 206 | }); 207 | 208 | describe('When action type prefixing is deactivated', () => { 209 | @StoreChunk({ 210 | feature: 'counterSlice', 211 | enableActionTypePrefixing: false, 212 | defaults: { count: 0 } 213 | }) 214 | class Counter { 215 | increment = createDuck('Increment'); 216 | } 217 | 218 | it('avoids prefixing action type if deactivated due to configuration', () => { 219 | TestBed.resetTestingModule(); 220 | TestBed.configureTestingModule({ imports: [StoreModule.forRoot({})] }); 221 | 222 | const counter = TestBed.inject(Counter); 223 | 224 | expect(counter.increment().type).toBe('Increment'); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **smver:** fix typo in parameter ([aa98655](https://github.com/co-IT/ngrx-ducks/commit/aa986559cf4089642f41ba0b66b430c2066ecac9)) 11 | 12 | 13 | ### Features 14 | 15 | * **core:** require rxjs 7 ([7e63f94](https://github.com/co-IT/ngrx-ducks/commit/7e63f9474c777c0b597ca38fe1d5857a49739583)) 16 | 17 | 18 | 19 | # [16.0.0](https://github.com/co-IT/ngrx-ducks/compare/v15.0.0...v16.0.0) (2023-06-21) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **smver:** fix typo in parameter ([aa98655](https://github.com/co-IT/ngrx-ducks/commit/aa986559cf4089642f41ba0b66b430c2066ecac9)) 25 | 26 | 27 | ### Features 28 | 29 | * **core:** require rxjs 7 ([7e63f94](https://github.com/co-IT/ngrx-ducks/commit/7e63f9474c777c0b597ca38fe1d5857a49739583)) 30 | 31 | 32 | 33 | # [15.0.0](https://github.com/co-IT/ngrx-ducks/compare/v14.0.3...v15.0.0) (2022-11-29) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * apply work around for useActions ([842a142](https://github.com/co-IT/ngrx-ducks/commit/842a14225895f479ba506f5ab470c62de7b20306)) 39 | * remove schematics from README ([7c64b67](https://github.com/co-IT/ngrx-ducks/commit/7c64b67d317379c56685e8867da617826bca981a)) 40 | * resolve security risks ([cd2d4f0](https://github.com/co-IT/ngrx-ducks/commit/cd2d4f038ffe47cbff72333dfdb5df8552c28a2b)) 41 | 42 | 43 | ### Features 44 | 45 | * **getActions:** remove deprecated helper getActions ([e1c40df](https://github.com/co-IT/ngrx-ducks/commit/e1c40dffd41933e5897bedfa5fa4db6f61096d33)) 46 | * **getActions:** remove deprecated helper usePicks ([8225796](https://github.com/co-IT/ngrx-ducks/commit/8225796d98e7c5b4b2bacf430ae82e684870ce74)) 47 | * **StoreChunk:** remove deprecated StoreFacade-Decorator ([1ceba3b](https://github.com/co-IT/ngrx-ducks/commit/1ceba3bf397707b8214c93065021b1fd9dfaadad)) 48 | * **useActions:** provide a comprehensible Error if no contructor is provided ([6a72f24](https://github.com/co-IT/ngrx-ducks/commit/6a72f24897a83f032948e4f6773be12bf28814e0)) 49 | * **useSelectors:** remove deprecated helper bindSelectors ([354cf9a](https://github.com/co-IT/ngrx-ducks/commit/354cf9a432e5b11a46142271cf17c4f893c53309)) 50 | 51 | 52 | ### BREAKING CHANGES 53 | 54 | * **useSelectors:** bindSelectors has to be replaced with useSelectors 55 | * **getActions:** usePick has to be replaced useSelect 56 | * **getActions:** getActions has to be replaced useActions 57 | * **StoreChunk:** @StoreFacade has to be replaced with @StoreChunk 58 | 59 | 60 | 61 | ## [14.0.3](https://github.com/co-IT/ngrx-ducks/compare/v14.0.2...v14.0.3) (2022-11-29) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * **core:** correct version range of rxjs ([4737bbe](https://github.com/co-IT/ngrx-ducks/commit/4737bbe3b866004e471d97e6459bab32245de645)) 67 | 68 | 69 | 70 | ## [14.0.2](https://github.com/co-IT/ngrx-ducks/compare/v14.0.1...v14.0.2) (2022-11-21) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * correct peerDependency to Angular ([80e7acf](https://github.com/co-IT/ngrx-ducks/commit/80e7acf3020b9bb293698c25b40f968cd0b84d1f)) 76 | 77 | 78 | 79 | ## [14.0.1](https://github.com/co-IT/ngrx-ducks/compare/v14.0.0...v14.0.1) (2022-11-19) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * add rxjs as peerDependency ([ecfae1b](https://github.com/co-IT/ngrx-ducks/commit/ecfae1b5d16be3496f4e29512b9c65322b423c08)) 85 | * update link to StackBlitz ([c71fd82](https://github.com/co-IT/ngrx-ducks/commit/c71fd823602877202567f469d6b29e45e4f88c1a)) 86 | 87 | 88 | 89 | # [14.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.1.3...v14.0.0) (2022-06-11) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * run security audit ([64fa009](https://github.com/co-IT/ngrx-ducks/commit/64fa009c5becd326ca05b69043070fc34924244b)) 95 | 96 | 97 | ### Features 98 | 99 | * connect to nx cloud ([efff830](https://github.com/co-IT/ngrx-ducks/commit/efff8302463de12948dab82f18be19436d58a118)) 100 | * define peer versions ([cf61db8](https://github.com/co-IT/ngrx-ducks/commit/cf61db87da28baa810f838fff137d1586ca388f7)) 101 | * migrate to Angular 14 ([606b5b8](https://github.com/co-IT/ngrx-ducks/commit/606b5b8287ebc7140df4604a2ebf630f50ee1a87)) 102 | * remove schematics library ([383566b](https://github.com/co-IT/ngrx-ducks/commit/383566bd3d4081a1ba491eef6cb727cc4212d5cf)) 103 | 104 | 105 | 106 | ## [13.1.3](https://github.com/co-IT/ngrx-ducks/compare/v13.1.2...v13.1.3) (2022-03-14) 107 | 108 | 109 | 110 | ## [13.1.2](https://github.com/co-IT/ngrx-ducks/compare/v13.1.1...v13.1.2) (2022-03-14) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * add missing name ([2421ecf](https://github.com/co-IT/ngrx-ducks/commit/2421ecfb1f1dfd4989fc965405a33192f210d6bf)) 116 | * correct intendation of env ([2baa72c](https://github.com/co-IT/ngrx-ducks/commit/2baa72cd8fd0fbf18f31c6140a15b3bb3bf57bb6)) 117 | * **store-registration:** flatten existing reducers when combining them ([bfa8a92](https://github.com/co-IT/ngrx-ducks/commit/bfa8a9244d08a715516508a67a7f9a5756dedb6e)) 118 | 119 | 120 | 121 | ## [13.1.1](https://github.com/co-IT/ngrx-ducks/compare/v13.1.0...v13.1.1) (2022-02-11) 122 | 123 | 124 | 125 | # [13.1.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.2...v13.1.0) (2022-02-11) 126 | 127 | 128 | ### Bug Fixes 129 | 130 | * add missing ngrx dependency ([22ad8f7](https://github.com/co-IT/ngrx-ducks/commit/22ad8f79b85597179f2e6d760c3864b130c9aeb4)) 131 | * get example app to run ([32106db](https://github.com/co-IT/ngrx-ducks/commit/32106db190d1d4ef238a0d88da76e94ed1ba1210)) 132 | 133 | 134 | ### Features 135 | 136 | * add helper checking for props ([5fe6157](https://github.com/co-IT/ngrx-ducks/commit/5fe61577bf5d5518b132c000f48f3a1bbd33f756)) 137 | * add nested duck example ([e614b18](https://github.com/co-IT/ngrx-ducks/commit/e614b18bb7f5d4ab0489e1ade0074c8939c1601c)) 138 | * **recursive:** ignore duck-blocks ([6770d91](https://github.com/co-IT/ngrx-ducks/commit/6770d91471a2b3a5f84a64f1c7eeaf14fc31adac)) 139 | 140 | 141 | 142 | ## [13.0.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.1...v13.0.2) (2022-01-13) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * **has-immutable-duck:** consider nested ducks ([c00b243](https://github.com/co-IT/ngrx-ducks/commit/c00b243c180d6eae8597c0f318623c512561e4c1)) 148 | 149 | 150 | 151 | ## [13.0.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0...v13.0.1) (2021-12-14) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * restore README files ([a0830f7](https://github.com/co-IT/ngrx-ducks/commit/a0830f7ac821eb01aa1b2de7b0767dd258ab3b40)) 157 | 158 | 159 | 160 | # [13.0.0](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.3...v13.0.0) (2021-12-14) 161 | 162 | 163 | 164 | # [13.0.0-alpha.3](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.2...v13.0.0-alpha.3) (2021-12-14) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **use-actions:** add type to actionCreator ([6733305](https://github.com/co-IT/ngrx-ducks/commit/6733305571dd31928ce5fa6470abb214cf4bdf7c)) 170 | 171 | 172 | 173 | # [13.0.0-alpha.2](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.1...v13.0.0-alpha.2) (2021-12-14) 174 | 175 | 176 | ### Features 177 | 178 | * **action-prefixung:** ➕ possibility to disable automatic prefixing ([8501330](https://github.com/co-IT/ngrx-ducks/commit/85013303acd6d84e99ec43b8cffce0d3daafb768)) 179 | * **actions:** auto-prefix action types 🚀 ([4006ed4](https://github.com/co-IT/ngrx-ducks/commit/4006ed4e2f4b05f76335aab3bf767d25d09d3741)) 180 | 181 | 182 | 183 | # [13.0.0-alpha.1](https://github.com/co-IT/ngrx-ducks/compare/v13.0.0-alpha.0...v13.0.0-alpha.1) (2021-12-13) 184 | 185 | 186 | ### Features 187 | 188 | * hamronize naming by introducing StoreChunk ([613eb4e](https://github.com/co-IT/ngrx-ducks/commit/613eb4e95fc3a929c4dfc49e47803d2a32cb4775)) 189 | * hamronize naming by introducing useActions ([f7db2b7](https://github.com/co-IT/ngrx-ducks/commit/f7db2b7854df42f35807b44cfa83cba0ab8426a3)) 190 | * hamronize naming by introducing useSelect ([496c9d5](https://github.com/co-IT/ngrx-ducks/commit/496c9d50ff7be273f36bd30a10f8f1492cf96e59)) 191 | * hamronize naming by introducing useSelectors ([4cb230e](https://github.com/co-IT/ngrx-ducks/commit/4cb230eb5e91be1b544a14573ebea70fefe9c97d)) 192 | 193 | 194 | 195 | # [13.0.0-alpha.0](https://github.com/co-IT/ngrx-ducks/compare/v12.4.3...v13.0.0-alpha.0) (2021-12-13) 196 | 197 | 198 | ### Bug Fixes 199 | 200 | * add missin eslintrc ([5b632f7](https://github.com/co-IT/ngrx-ducks/commit/5b632f71a280e37bbfe34d3786a2d70c74600ba7)) 201 | --------------------------------------------------------------------------------