diff --git a/.gitignore b/.gitignore index ef0a3f8a8..add84899c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist node_modules test-reports tmp + +*.iml diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000..5426a9320 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx commitlint --edit $1 diff --git a/.husky/post-commit b/.husky/post-commit new file mode 100755 index 000000000..ec1343088 --- /dev/null +++ b/.husky/post-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +git update-index --again diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..4b5e8e292 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged +npm run lint +npm run test diff --git a/docs/articles/api/core/to-factory-selector.md b/docs/articles/api/core/to-factory-selector.md new file mode 100644 index 000000000..7a5dafdb6 --- /dev/null +++ b/docs/articles/api/core/to-factory-selector.md @@ -0,0 +1,34 @@ +--- +title: toFactorySelector +description: Information about toFactorySelector function and how to create a factory selector function +sidebar_label: toFactorySelector +--- + +`toFactorySelector` helps to create a factory selector function from a root selector. +In this case, the produced selector can be passed directly to NGRX `store.select`. + +This function is useful for NGRX v12 and younger. + +```ts +export class MyComponent { + public readonly users$: Observable; + + private readonly selectUser = + toFactorySelector( + rootUser( + relUserCompany( + relCompanyAddress(), + ), + ), + ); + + constructor(private store: Store) { + this.users$ = this.store.select( + // let's select current user + this.selectUser( + selectCurrentUserId, + ), + ); + } +} +``` diff --git a/docs/articles/api/core/to-static-selector.md b/docs/articles/api/core/to-static-selector.md new file mode 100644 index 000000000..ca40f03db --- /dev/null +++ b/docs/articles/api/core/to-static-selector.md @@ -0,0 +1,33 @@ +--- +title: toStaticSelector +description: Information about toStaticSelector function and how to bind param to a root selector +sidebar_label: toStaticSelector +--- + +`toStaticSelector` helps to create a selector function from a root selector. +Its behavior very similar to [`toFactorySelector`](./to-factory-selector.md), +with the difference that the passed params are static and cannot be changed. + +This function is useful for NGRX v12 and younger. + +```ts +export class MyComponent { + public readonly users$: Observable; + + private readonly selectCurrentUser = + toStaticSelector( + rootUser( + relUserCompany( + relCompanyAddress(), + ), + ), + selectCurrentUserId, + ); + + constructor(private store: Store) { + this.users$ = this.store.select( + this.selectCurrentUser, + ); + } +} +``` diff --git a/docs/articles/guide/ngrx-data.md b/docs/articles/guide/ngrx-data.md index b17815c20..4680903f7 100644 --- a/docs/articles/guide/ngrx-data.md +++ b/docs/articles/guide/ngrx-data.md @@ -110,12 +110,16 @@ export class MyComponent { userSelectors: UserSelectorService, ) { this.user$ = store.select( - userSelectors.selectUser, - '1', + toStaticSelector( + userSelectors.selectUser, + '1', + ), ); this.users$ = store.select( - userSelectors.selectUsers, - ['1', '2'], + toStaticSelector( + userSelectors.selectUsers, + ['1', '2'], + ), ); } } diff --git a/docs/articles/guide/quick.md b/docs/articles/guide/quick.md index 17b7cc5c6..cdd10438b 100644 --- a/docs/articles/guide/quick.md +++ b/docs/articles/guide/quick.md @@ -55,8 +55,7 @@ the only requirement is the `Dictionary` (a regular object). The next step is to define functions which select the state of an entity. In this library, they are called [**entity state selectors**](../api/core/entity-state-selector.md). -```ts -// Redux +```ts title="Redux" export const selectUserState = state => state.users; export const selectCompanyState = state => state.companies; // `stateKeys` function helps in case of different names of the properties. @@ -67,8 +66,7 @@ export const selectAddressState = stateKeys( ); ``` -```ts -// NGRX +```ts title="NGRX" export const selectUserState = createFeatureSelector( 'users', @@ -165,8 +163,7 @@ In case of arrays, such as `company.staff`, there is [`childrenEntitiesSelector` Now, let's go to a component where we want to select a user with relationships, and create a **root selector** via the factories there: -```ts -// Redux +```ts title="Redux" const selectUser = rootUser( relUserCompany( relCompanyAddress(), @@ -182,8 +179,32 @@ const mapStateToProps = state => { export default connect(mapStateToProps)(MyComponent); ``` -```ts -// NGRX +```ts title="NGRX 12 and younger" +export class MyComponent { + public readonly users$: Observable; + + private readonly selectUser = + rootUser( + relUserCompany( + relCompanyAddress(), + ), + ); + + constructor(private store: Store) { + this.users$ = this.store.select( + // toStaticSelector should be used + // to create a selector for v12 + toStaticSelector( + this.selectUser, + // '1' is the id of user + '1', + ), + ); + } +} +``` + +```ts title="NGRX 11 and older" export class MyComponent { public readonly users$: Observable; @@ -205,13 +226,20 @@ export class MyComponent { Of course, instead of a hardcoded id like `1`, we can pass another **selector, that selects ids** from the state. -```ts -// Redux +```ts title="Redux" selectUser(state, selectCurrentUserId); ``` -```ts -// NGRX +```ts title="NGRX 12 and younger" +this.store.select( + toStaticSelector( + this.selectUser, + selectCurrentUserId, + ), +); +``` + +```ts title="NGRX 11 and older" this.store.select(this.selectUser, selectCurrentUserId); ``` @@ -234,13 +262,20 @@ const selectUsers = rootEntities(selectUser); Now we can use `selectUsers` in our components, but instead of an id, it requires an array of them. -```ts -// Redux +```ts title="Redux" selectUsers(state, ['1', '2']); ``` -```ts -// NGRX +```ts title="NGRX 12 and younger" +this.store.select( + toStaticSelector( + this.selectUsers, + ['1', '2'], + ), +); +``` + +```ts title="NGRX and older" this.store.select(this.selectUsers, ['1', '2']); ``` diff --git a/docs/articles/index.md b/docs/articles/index.md index 88b0dfeda..e080849ab 100644 --- a/docs/articles/index.md +++ b/docs/articles/index.md @@ -19,9 +19,10 @@ The current version of `ngrx-entity-relationship` has been tested and can be use - Redux 4, React Redux 7, **try it live on [StackBlitz](https://stackblitz.com/edit/ngrx-entity-relationship-react?file=src/MyComponent.tsx) or [CodeSandbox](https://codesandbox.io/s/github/satanTime/ngrx-entity-relationship-react?file=/src/MyComponent.tsx)** - -- NGRX 10, **try it live on [StackBlitz](https://stackblitz.com/github/satanTime/ngrx-entity-relationship-angular?file=src/app/app.component.ts) +- NGRX 12, **try it live on [StackBlitz](https://stackblitz.com/github/satanTime/ngrx-entity-relationship-angular?file=src/app/app.component.ts) or [CodeSandbox](https://codesandbox.io/s/github/satanTime/ngrx-entity-relationship-angular?file=/src/app/app.component.ts)** +- NGRX 11 +- NGRX 10 - NGRX 9 - NGRX 8 - NGRX 7 diff --git a/docs/sidebars.js b/docs/sidebars.js index 21d0f12b1..8c35a83ae 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -51,6 +51,8 @@ module.exports = { 'api/core/childentityselector-function', 'api/core/childrenentities-function', 'api/core/childrenentitiesselector-function', + 'api/core/to-factory-selector', + 'api/core/to-static-selector', ], }, { diff --git a/e2e/a12/src/app/entity/entity.component.ts b/e2e/a12/src/app/entity/entity.component.ts index 036faaaf6..e78c39713 100644 --- a/e2e/a12/src/app/entity/entity.component.ts +++ b/e2e/a12/src/app/entity/entity.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, Component, OnDestroy} from '@angular/core'; import {select, Store} from '@ngrx/store'; -import {rootEntities} from 'ngrx-entity-relationship'; +import {rootEntities, toFactorySelector} from 'ngrx-entity-relationship'; import {combineLatest, Observable} from 'rxjs'; import {filter, map, switchMap} from 'rxjs/operators'; import {Company} from './store/company/company.model'; @@ -29,7 +29,7 @@ import {User} from './store/user/user.model'; export class EntityComponent implements OnDestroy { public readonly company$: Observable; // prettier-ignore - private readonly companyWithCrazyData = rootCompany( + private readonly companyWithCrazyData = toFactorySelector(rootCompany( relCompanyAddress(), relCompanyAdmin( relUserEmployees(), @@ -41,25 +41,25 @@ export class EntityComponent implements OnDestroy { ), ), ), - ); + )); public readonly users$: Observable>; // prettier-ignore - private readonly users = rootEntities( + private readonly users = toFactorySelector(rootEntities( rootUser( relUserEmployees( relUserManager(), ), relUserManager(), ), - ); + )); constructor(protected readonly store: Store, public readonly entitiesService: EntityService) { this.users$ = combineLatest([ - this.store.select(this.users, selectCurrentUsersIds), + this.store.select(this.users(selectCurrentUsersIds)), this.store.pipe( select(selectCurrentUsersIds), - switchMap(ids => this.store.select(this.users, ids)), + switchMap(ids => this.store.select(this.users(ids))), ), ]).pipe( filter(([a, b]) => a === b), @@ -67,10 +67,10 @@ export class EntityComponent implements OnDestroy { ); this.company$ = combineLatest([ - this.store.select(this.companyWithCrazyData, selectCurrentCompanyId), + this.store.select(this.companyWithCrazyData(selectCurrentCompanyId)), this.store.pipe( select(selectCurrentCompanyId), - switchMap(id => this.store.select(this.companyWithCrazyData, id)), + switchMap(id => this.store.select(this.companyWithCrazyData(id))), ), ]).pipe( filter(([a, b]) => a === b), diff --git a/e2e/a12/src/app/entity/store/entity.service.ts b/e2e/a12/src/app/entity/store/entity.service.ts index e49080467..543988000 100644 --- a/e2e/a12/src/app/entity/store/entity.service.ts +++ b/e2e/a12/src/app/entity/store/entity.service.ts @@ -1,5 +1,6 @@ import {Injectable} from '@angular/core'; import {Store} from '@ngrx/store'; +import {toStaticSelector} from 'ngrx-entity-relationship'; import {filter, first, tap} from 'rxjs/operators'; import {UpdateAddress} from './address/address.actions'; import {Address} from './address/address.model'; @@ -15,7 +16,7 @@ export class EntityService { public changeUser(id: string): void { this.store - .select(rootUser(), id) + .select(toStaticSelector(rootUser(), id)) .pipe( filter((v): v is User => !!v), first(), @@ -40,7 +41,7 @@ export class EntityService { public changeCompany(id: string): void { this.store - .select(rootCompany(), id) + .select(toStaticSelector(rootCompany(), id)) .pipe( filter((v): v is Company => !!v), first(), @@ -65,7 +66,7 @@ export class EntityService { public changeAddress(id: string): void { this.store - .select(rootAddress(), id) + .select(toStaticSelector(rootAddress(), id)) .pipe( filter((v): v is Address => !!v), first(), diff --git a/libs/ngrx-entity-relationship/rxjs/src/operators/relationships.ts b/libs/ngrx-entity-relationship/rxjs/src/operators/relationships.ts index d42cf56cc..baf871aae 100644 --- a/libs/ngrx-entity-relationship/rxjs/src/operators/relationships.ts +++ b/libs/ngrx-entity-relationship/rxjs/src/operators/relationships.ts @@ -1,9 +1,9 @@ -import {HANDLER_ROOT_ENTITIES, HANDLER_ROOT_ENTITY, ID_TYPES} from 'ngrx-entity-relationship'; +import {HANDLER_ROOT_ENTITIES, HANDLER_ROOT_ENTITY, ID_TYPES, toFactorySelector} from 'ngrx-entity-relationship'; import {iif, Observable, of} from 'rxjs'; import {map, switchMap} from 'rxjs/operators'; export interface STORE_INSTANCE { - select(mapFn: (state: T, props: Props) => K, props: Props): Observable; + select(mapFn: (state: T) => K): Observable; } export function relationships( @@ -30,6 +30,8 @@ export function relationships( store: STORE_INSTANCE, selector: HANDLER_ROOT_ENTITY, ): (next: Observable) => Observable { + const factory = toFactorySelector(selector); + return next => next.pipe( switchMap(input => { @@ -48,7 +50,7 @@ export function relationships( } return selector.idSelector(set) as any as TYPES; }), - switchMap(id => store.select(selector, id)), + switchMap(id => store.select(factory(id))), ), ); }), diff --git a/libs/ngrx-entity-relationship/src/lib/toFactorySelector.ts b/libs/ngrx-entity-relationship/src/lib/toFactorySelector.ts new file mode 100644 index 000000000..a1b5c1778 --- /dev/null +++ b/libs/ngrx-entity-relationship/src/lib/toFactorySelector.ts @@ -0,0 +1,26 @@ +export function toFactorySelector(selector: { + (state: S, ids: Props): K; + release?: () => void; +}): { + (ids: Props): { + (state: S): K; + release(): void; + }; + release(): void; +} { + const release = () => { + if (typeof selector.release === 'function') { + selector.release(); + } + }; + + const callback = (ids: Props) => { + const storeSelector = (state: S) => selector(state, ids); + storeSelector.release = release; + + return storeSelector; + }; + callback.release = release; + + return callback; +} diff --git a/libs/ngrx-entity-relationship/src/lib/toStaticSelector.ts b/libs/ngrx-entity-relationship/src/lib/toStaticSelector.ts new file mode 100644 index 000000000..a6a64b070 --- /dev/null +++ b/libs/ngrx-entity-relationship/src/lib/toStaticSelector.ts @@ -0,0 +1,14 @@ +import {toFactorySelector} from './toFactorySelector'; + +export function toStaticSelector( + selector: { + (state: S, ids: Props): K; + release?: () => void; + }, + ids: Props, +): { + (state: S): K; + release(): void; +} { + return toFactorySelector(selector)(ids); +} diff --git a/libs/ngrx-entity-relationship/src/public_api.ts b/libs/ngrx-entity-relationship/src/public_api.ts index c0203c9f8..6de5f1604 100644 --- a/libs/ngrx-entity-relationship/src/public_api.ts +++ b/libs/ngrx-entity-relationship/src/public_api.ts @@ -16,6 +16,8 @@ export {ngrxEntityRelationshipReducer} from './lib/store/ngrxEntityRelationshipR export {selectByIds} from './lib/selectByIds'; export {stateKeys} from './lib/stateKeys'; +export {toFactorySelector} from './lib/toFactorySelector'; +export {toStaticSelector} from './lib/toStaticSelector'; export {isBuiltInSelector, isSelectorMeta} from './lib/types'; diff --git a/libs/ngrx-entity-relationship/test/rxjs/src/operators/relationships.spec.ts b/libs/ngrx-entity-relationship/test/rxjs/src/operators/relationships.spec.ts index 80eecf533..c8eb277b2 100644 --- a/libs/ngrx-entity-relationship/test/rxjs/src/operators/relationships.spec.ts +++ b/libs/ngrx-entity-relationship/test/rxjs/src/operators/relationships.spec.ts @@ -47,7 +47,7 @@ describe('operators/relationships', () => { .subscribe(actual => { expect(actual).toBe(expected as any); expect(selector.idSelector).toHaveBeenCalledWith(entity); - expect(store.select).toHaveBeenCalledWith(selector, 'hello'); + expect(store.select).toHaveBeenCalledWith(jasmine.anything()); store$.complete(); doneFn(); }); @@ -80,7 +80,7 @@ describe('operators/relationships', () => { expect(actual).toBe(expected as any); expect(selector.idSelector).toHaveBeenCalledWith(entity1); expect(selector.idSelector).toHaveBeenCalledWith(entity2); - expect(store.select).toHaveBeenCalledWith(selector, ['hello1', 'hello2']); + expect(store.select).toHaveBeenCalledWith(jasmine.anything()); store$.complete(); doneFn(); }); diff --git a/libs/ngrx-entity-relationship/test/src/lib/toFactorySelector.spec.ts b/libs/ngrx-entity-relationship/test/src/lib/toFactorySelector.spec.ts new file mode 100644 index 000000000..6b97d3892 --- /dev/null +++ b/libs/ngrx-entity-relationship/test/src/lib/toFactorySelector.spec.ts @@ -0,0 +1,31 @@ +import {toFactorySelector} from '../../../src/lib/toFactorySelector'; + +describe('toFactorySelector', () => { + it('creates a factory function', () => { + const selector = jasmine.createSpy('selector').and.returnValue('result'); + const factory = toFactorySelector(selector); + const storeSelector = factory('param'); + const state = {}; + + const actual = storeSelector(state); + expect(actual).toEqual('result'); + expect(selector).toHaveBeenCalledWith(state, 'param'); + }); + + it('provides a release function', () => { + const selector = jasmine.createSpy('selector').and.returnValue('return'); + const factory = toFactorySelector(selector); + expect(factory.release).not.toThrow(); + }); + + it('provides a release function which delegates the call', () => { + const selector: { + (state: unknown, ids: unknown): unknown; + release?: () => void; + } = jasmine.createSpy('selector').and.returnValue('return'); + (selector as any).release = jasmine.createSpy('selector.release'); + const factory = toFactorySelector(selector); + expect(factory.release).not.toThrow(); + expect(selector.release).toHaveBeenCalled(); + }); +}); diff --git a/libs/ngrx-entity-relationship/test/src/lib/toStaticSelector.spec.ts b/libs/ngrx-entity-relationship/test/src/lib/toStaticSelector.spec.ts new file mode 100644 index 000000000..d5c7d03e3 --- /dev/null +++ b/libs/ngrx-entity-relationship/test/src/lib/toStaticSelector.spec.ts @@ -0,0 +1,30 @@ +import {toStaticSelector} from '../../../src/lib/toStaticSelector'; + +describe('toStaticSelector', () => { + it('creates a factory function', () => { + const selector = jasmine.createSpy('selector').and.returnValue('result'); + const storeSelector = toStaticSelector(selector, 'param'); + const state = {}; + + const actual = storeSelector(state); + expect(actual).toEqual('result'); + expect(selector).toHaveBeenCalledWith(state, 'param'); + }); + + it('provides a release function', () => { + const selector = jasmine.createSpy('selector').and.returnValue('return'); + const factory = toStaticSelector(selector, 'param'); + expect(factory.release).not.toThrow(); + }); + + it('provides a release function which delegates the call', () => { + const selector: { + (state: unknown, ids: unknown): unknown; + release?: () => void; + } = jasmine.createSpy('selector').and.returnValue('return'); + (selector as any).release = jasmine.createSpy('selector.release'); + const factory = toStaticSelector(selector, 'param'); + expect(factory.release).not.toThrow(); + expect(selector.release).toHaveBeenCalled(); + }); +}); diff --git a/package.json b/package.json index 440d82ebb..dd0df2c5e 100644 --- a/package.json +++ b/package.json @@ -179,13 +179,6 @@ "type": "git", "url": "https://github.com/satanTime/ngrx-entity-relationship" }, - "husky": { - "hooks": { - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", - "pre-commit": "lint-staged && npm run lint && npm run test", - "post-commit": "git update-index --again" - } - }, "commitlint": { "extends": [ "@commitlint/config-conventional"