diff --git a/src/core/types.ts b/src/core/types.ts index f83cb6e..bdbfeec 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -32,3 +32,6 @@ export interface Hooks { export type AtLeastOne }> = Partial & U[keyof U]; +export interface NonEmptyArray extends Array { + 0: A; +} diff --git a/src/core/variant.ts b/src/core/variant.ts index e799967..f0559e7 100644 --- a/src/core/variant.ts +++ b/src/core/variant.ts @@ -7,12 +7,79 @@ import { BindableProps, Context, Hooks, + NonEmptyArray, PartialBoundProps, View, } from './types'; +type SourceKeysCasesConfig< + Props, + Variant extends string, + Bind extends BindableProps, +> = { + source: Store; + bind?: Bind; + cases: AtLeastOne>>; + hooks?: Hooks; + default?: View; +}; + +type ArrayCasesConfig> = { + source: Store; + cases: NonEmptyArray<{ filter: (source: Source) => boolean; view: View }>; + bind?: Bind; + hooks?: Hooks; + default?: View; +}; + +type Config< + Props, + Variant extends string, + Bind extends BindableProps, + Source, +> = + | ArrayCasesConfig + | SourceKeysCasesConfig + | { + if: Store; + then: View; + else?: View; + hooks?: Hooks; + bind?: Bind; + }; + const Default = () => null; +function isArrayCasesConfig< + Props, + Variant extends string, + Bind extends BindableProps, + Source, +>( + config: Config, +): config is ArrayCasesConfig { + if ('cases' in config) { + return Array.isArray(config.cases); + } + + return false; +} + +function isSourceKeysCasesConfig< + Props, + Variant extends string, + Bind extends BindableProps, + Source, +>( + config: Config, +): config is SourceKeysCasesConfig { + if ('cases' in config) { + return !Array.isArray(config.cases); + } + + return false; +} + export function variantFactory(context: Context) { const reflect = reflectFactory(context); @@ -20,49 +87,54 @@ export function variantFactory(context: Context) { Props, Variant extends string, Bind extends BindableProps, + Source, >( - config: - | { - source: Store; - bind?: Bind; - cases: AtLeastOne>>; - hooks?: Hooks; - default?: View; - } - | { - if: Store; - then: View; - else?: View; - hooks?: Hooks; - bind?: Bind; - }, + config: Config, ): React.FC> { let $case: Store; let cases: AtLeastOne>>; let def: View; + let View: View; - // Shortcut for Store - if ('if' in config) { - $case = config.if.map((value): Variant => (value ? 'then' : 'else') as Variant); + if (isArrayCasesConfig(config)) { + View = (props: Props) => { + const source = context.useUnit(config.source); + let Component = config.default ?? Default; - cases = { - then: config.then, - else: config.else, - } as unknown as AtLeastOne>>; - def = Default; - } - // Full form for Store - else { - $case = config.source; - cases = config.cases; - def = config.default ?? Default; - } + for (const oneOfCases of config.cases) { + if (oneOfCases.filter(source)) { + Component = oneOfCases.view; + } + } + + return React.createElement(Component as any, props as any); + }; + } else { + // Shortcut for Store + if ('if' in config) { + $case = config.if.map( + (value): Variant => (value ? 'then' : 'else') as Variant, + ); + + cases = { + then: config.then, + else: config.else, + } as unknown as AtLeastOne>>; + def = Default; + } + // Full form for Store + else if (isSourceKeysCasesConfig(config)) { + $case = config.source; + cases = config.cases; + def = config.default ?? Default; + } - function View(props: Props) { - const nameOfCase = context.useUnit($case); - const Component = cases[nameOfCase] ?? def; + View = (props: Props) => { + const nameOfCase = context.useUnit($case); + const Component = cases[nameOfCase] ?? def; - return React.createElement(Component as any, props as any); + return React.createElement(Component as any, props as any); + }; } const bind = config.bind ?? ({} as Bind); diff --git a/type-tests/types-variant.tsx b/type-tests/types-variant.tsx index f54f402..0732ef9 100644 --- a/type-tests/types-variant.tsx +++ b/type-tests/types-variant.tsx @@ -85,6 +85,16 @@ import { variant } from '../src'; }); expectType(CurrentPage); + + const Page = variant({ + source: $page, + bind: { context: $pageContext }, + // @ts-expect-error + cases: [], + default: NotFoundPage, + }); + + expectType(Page); } // variant allows to set every possble case @@ -143,6 +153,37 @@ import { variant } from '../src'; expectType(CurrentPage); } +// overload for cases as array +{ + type PageProps = { + context: { + route: string; + }; + }; + + const $ctx = createStore({ route: 'home' }); + + const UserProfile: React.FC = () => null; + const AdminProfile: React.FC = () => null; + const $user = createStore({ isAdmin: false }); + + const Profile = variant({ + source: $user, + cases: [ + { + filter: (user) => user.isAdmin, + view: UserProfile, + }, + { + filter: (user) => !user.isAdmin, + view: AdminProfile, + }, + ], + bind: { context: $ctx }, + }); + expectType(Profile); +} + // overload for boolean source { type PageProps = {