Last active
January 13, 2021 10:56
-
-
Save Carniatto/0e677ec9b43f949c07538e4c69e1f9ce to your computer and use it in GitHub Desktop.
State helpers for selectors creation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* These are some example usages */ | |
import { createPropertySelectors, createMappedSelector } from "./ngxs-next"; | |
interface AppStateModel { | |
firstName: string, | |
lastName: string | |
} | |
interface AuthStateModel { | |
logged: boolean, | |
grants: { | |
read: boolean, | |
write: boolean, | |
delete: boolean | |
} | |
} | |
// app.state.selectors.ts | |
export class AppStateSelectors { | |
/* Case1: Creating base props selectors from a state */ | |
static props = createPropertySelectors<AppStateModel>(AppState); | |
@Selector([AppStateSelectors.props.firstName, AppStateSelectors.props.lastName]) | |
welcomeMsg(fistName: string, lastName: string) { | |
return `Welccome ${firstName} ${lastName}; | |
} | |
} | |
// auth.state.selectors.ts | |
export class AuthStateSelectors { | |
static props = createPropertySelectors<AuthStateModel>(AuthState); | |
} | |
/* Case2: Creating a mapped selector from selectors */ | |
const authUser = createMappedSelector({ | |
firstName: AppStateSelectors.props.firstName, | |
logged: AuthStateSelectors.props.logged | |
}) | |
/* Case3: Creating a selector by pick properties */ | |
const readWriteGrants = createPickSelector(AuthStateSelectors.props.grants, ['read', 'write']) | |
const adminGrants = createPickSelector(AuthStateSelectors.props.grants, ['read', 'write', 'delete']) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Just copy this file to your project and start experimenting */ | |
import { createSelector, StateToken } from '@ngxs/store'; | |
import { StateClass } from '@ngxs/store/internals'; | |
type SelectorOnly<TModel> = StateToken<TModel> | ((...arg: any) => TModel); | |
type Selector<TModel> = StateClass<any> | SelectorOnly<TModel>; | |
export type PropertySelectors<TModel> = { | |
[P in keyof TModel]: (model: TModel) => TModel[P]; | |
}; | |
export function createPropertySelectors<TModel>( | |
state: Selector<TModel>, | |
): PropertySelectors<TModel> { | |
const cache: Partial<PropertySelectors<TModel>> = {}; | |
return new Proxy( | |
{}, | |
{ | |
get(target: any, prop: string) { | |
const selector = cache[prop] || createSelector([state], (s: TModel) => s?.[prop]); | |
cache[prop] = selector; | |
return selector; | |
}, | |
}, | |
); | |
} | |
interface SelectorMap { | |
[key: string]: SelectorOnly<any>; | |
} | |
type MappedSelector<T extends SelectorMap> = (...args: any[]) => MappedResult<T>; | |
type MappedResult<TSelectorMap> = { | |
[P in keyof TSelectorMap]: TSelectorMap[P] extends SelectorOnly<infer R> ? R : never; | |
}; | |
export function createMappedSelector<T extends SelectorMap>(selectorMap: T): MappedSelector<T> { | |
const selectors = Object.values(selectorMap); | |
return createSelector(selectors, (...args) => { | |
return Object.keys(selectorMap).reduce((obj, key, index) => { | |
(obj as any)[key] = args[index]; | |
return obj; | |
}, {} as MappedResult<T>); | |
}) as MappedSelector<T>; | |
} | |
export function createPickSelector<TModel, Key extends keyof TModel>( | |
state: Selector<TModel>, | |
keys: Key[] | |
) { | |
const selectors = keys.map((key) => createSelector([state], (s: TModel) => s[key])); | |
return createSelector([...selectors], (...selected: any) => { | |
return keys.reduce((acc, key, index) => { | |
acc[key] = selected[index]; | |
return acc; | |
}, {} as Pick<TModel, Key>); | |
}); | |
} |
Just a note that line 22 of ngxs-next.ts
might need to be different if you are using an older version of TypeScript.
Like so:
const selector = cache[prop] || createSelector([state], (s: TModel) => s && s[prop]);
@Carniatto
Found a bug in the createPickSelector
. It should be picking from the index in the selector arguments and not the key:
export function createPickSelector<TModel, Key extends keyof TModel>(
state: Selector<TModel>,
keys: Key[]
) {
const selectors = keys.map((key) => createSelector([state], (s: TModel) => s[key]));
return createSelector([...selectors], (...selected: any) => {
return keys.reduce((acc, key, index) => {
acc[key] = selected[index];
return acc;
}, {} as Pick<TModel, Key>);
});
}
Thanks @markwhitfeld Gist updated
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is an initial proposal for state slicers. Please provide your feedback in the issue here