@jswork/react-render-controls 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # react-render-controls
2
+ > A lightweight, headless React component library for declarative conditional rendering, pattern matching, and list mapping. SSR-friendly, zero UI, and fully type-safe.
3
+
4
+ ## installation
5
+ ```shell
6
+ yarn add @jswork/react-render-controls
7
+ ```
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var c=Object.defineProperty,A=Object.defineProperties;var M=Object.getOwnPropertyDescriptors;var d=Object.getOwnPropertySymbols;var V=Object.prototype.hasOwnProperty,b=Object.prototype.propertyIsEnumerable;var s=(t,e,n)=>e in t?c(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,p=(t,e)=>{for(var n in e||(e={}))V.call(e,n)&&s(t,n,e[n]);if(d)for(var n of d(e))b.call(e,n)&&s(t,n,e[n]);return t},m=(t,e)=>A(t,M(e)),o=(t,e)=>c(t,"name",{value:e,configurable:!0});var _react = require('react'); var _react2 = _interopRequireDefault(_react);var h,l=typeof process!="undefined"&&((h=process.env)==null?void 0:h.NODE_ENV)!=="production";function g(t){return typeof t=="function"}o(g,"isRenderFn");function x(t,e,n,r){if(r===void 0)return e;if(typeof r=="function")return r(t,e,n);let i=t[r];return i==null?(l&&console.warn(`RenderList: keyBy="${String(r)}" but the field is undefined in ${JSON.stringify(t)}`),e):i}o(x,"getKey");function R({items:t,render:e,keyBy:n}){return _react2.default.createElement(_react2.default.Fragment,null,t.map((r,i)=>{let f=x(r,i,t,n);if(g(e))return _react2.default.createElement(_react2.default.Fragment,{key:f},e(r,i,t));let{component:a,dataKey:w="item",props:C={}}=e,I=m(p({},C),{[w]:r});return _react2.default.createElement(_react2.default.Fragment,{key:f},_react2.default.createElement(a,I))}))}o(R,"RenderList");function F({when:t,children:e}){let n=_react.Children.toArray(e);return n.length===0?null:(n.length>2&&l&&console.warn(`RenderIf: Expected at most 2 children, but got ${n.length}. Only the first 2 children will be used.`),n.length===1?t?n[0]:null:t?n[0]:n[1])}o(F,"RenderIf");var S=F;function y(t,e){for(let n=0;n<e.length;n++){let r=e[n];if(typeof r=="string"){if(r===t)return n}else if(r.includes(t))return n}return-1}o(y,"findMatchIndex");function v(t){let e=[];for(let n of t)typeof n=="string"?e.push(n):e.push(...n);return e}o(v,"getAllValues");function L({value:t,items:e,children:n}){var f;let r=y(t,e);if(r===-1){if(l){let a=v(e);console.warn(`RenderMatch: Value "${t}" not found in any of the items. Available values: [${a.join(", ")}]`)}return null}let i=_react.Children.toArray(n).filter(a=>_react.isValidElement.call(void 0, a));return r>=i.length?(l&&console.warn(`RenderMatch: Not enough children provided. Expected at least ${r+1}, but got ${i.length}.`),null):(f=i[r])!=null?f:null}o(L,"RenderMatch");var O=L;function E(t){for(let e=0;e<t.length;e++)if(t[e])return e;return-1}o(E,"findTrueCaseIndex");function $(t,e,n){l&&t>e&&console.warn(`${n}: More cases (${t}) than children (${e}). Extra cases will be ignored.`)}o($,"validateCasesLength");function J({cases:t,children:e,fallback:n=null}){let r=_react.Children.toArray(e).filter(f=>_react.isValidElement.call(void 0, f));$(t.length,r.length,"RenderSwitch");let i=E(t);return i>=0&&i<r.length?r[i]:n}o(J,"RenderSwitch");var P=J;exports.RenderIf = S; exports.RenderList = R; exports.RenderMatch = O; exports.RenderSwitch = P;
2
+ //# sourceMappingURL=main.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/feizheng/github/react-render-controls/packages/lib/dist/main.cjs.js","../src/render-list/index.tsx","../src/shared/env.ts","../src/render-list/render-list.utils.ts"],"names":["_a","isDev","process","env","NODE_ENV","isRenderFn","render","getKey","item","index","items","keyBy","undefined","value","console","warn","String","JSON","stringify"],"mappings":"AAAA,6KAAI,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CCA3d,4EAAkB,ICAlBA,CAAAA,CAGaC,CAAAA,CACX,OAAOC,OAAAA,EAAY,WAAA,EAAA,CAAA,CAAeA,CAAAA,CAAAA,OAAAA,CAAQC,GAAAA,CAAAA,EAARD,IAAAA,CAAAA,KAAAA,CAAAA,CAAAA,CAAAA,CAAaE,QAAAA,CAAAA,GAAa,YAAA,CCEvD,SAASC,CAAAA,CAAcC,CAAAA,CAAqB,CACjD,OAAO,OAAOA,CAAAA,EAAW,UAC3B,CAFgBD,CAAAA,CAAAA,CAAAA,CAAAA,YAAAA,CAAAA,CAOT,SAASE,CAAAA,CACdC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CAA2B,CAE3B,EAAA,CAAIA,CAAAA,GAAUC,KAAAA,CAAAA,CACZ,OAAOH,CAAAA,CAGT,EAAA,CAAI,OAAOE,CAAAA,EAAU,UAAA,CACnB,OAAOA,CAAAA,CAAMH,CAAAA,CAAMC,CAAAA,CAAOC,CAAAA,CAAAA,CAG5B,IAAMG,CAAAA,CAASL,CAAAA,CAAiCG,CAAAA,CAAAA,CAChD,OAA2BE,CAAAA,EAAU,IAAA,CAAA,CAC/BZ,CAAAA,EACFa,OAAAA,CAAQC,IAAAA,CACN,CAAA,mBAAA,EAAsBC,MAAAA,CAAOL,CAAAA,CAAAA,CAAAA,gCAAAA,EAAyCM,IAAAA,CAAKC,SAAAA,CAAUV,CAAAA,CAAAA,CAAAA,CAAAA","file":"/Users/feizheng/github/react-render-controls/packages/lib/dist/main.cjs.js","sourcesContent":[null,"import React from 'react';\nimport type { RenderListProps } from './render-list.type';\nimport { isRenderFn, getKey } from './render-list.utils';\n\n/**\n * RenderList - Recommended list component\n *\n * @example Function style\n * ```tsx\n * <RenderList\n * items={users}\n * render={(user) => <UserCard user={user} />}\n * keyBy=\"id\"\n * />\n * ```\n *\n * @example Component style\n * ```tsx\n * <RenderList\n * items={users}\n * render={{\n * component: UserCard,\n * dataKey: \"user\",\n * props: { variant: 'compact' }\n * }}\n * keyBy=\"id\"\n * />\n * ```\n */\nexport default function RenderList<T>({ items, render, keyBy }: RenderListProps<T>) {\n return (\n <>\n {items.map((item, index) => {\n const key = getKey(item, index, items, keyBy);\n\n // If render is a function, call it directly\n if (isRenderFn(render)) {\n return <React.Fragment key={key}>{render(item, index, items)}</React.Fragment>;\n }\n\n // If render is component config, wrap props automatically\n const { component: Component, dataKey = 'item', props = {} } = render;\n const componentProps = {\n ...props,\n [dataKey]: item,\n };\n\n return (\n <React.Fragment key={key}>\n <Component {...componentProps} />\n </React.Fragment>\n );\n })}\n </>\n );\n}\n","/**\n * Check if running in development mode\n */\nexport const isDev =\n typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n","import type { RenderProp, RenderFn, KeyBy } from './render-list.type';\nimport { isDev } from '../shared/env';\n\n/**\n * Type guard: check if render is a function type\n */\nexport function isRenderFn<T>(render: RenderProp<T>): render is RenderFn<T> {\n return typeof render === 'function';\n}\n\n/**\n * Get the unique key for a list item\n */\nexport function getKey<T>(\n item: T,\n index: number,\n items: readonly T[],\n keyBy: KeyBy<T> | undefined,\n): string | number {\n if (keyBy === undefined) {\n return index;\n }\n\n if (typeof keyBy === 'function') {\n return keyBy(item, index, items);\n }\n\n const value = (item as Record<string, unknown>)[keyBy as string];\n if (value === undefined || value === null) {\n if (isDev) {\n console.warn(\n `RenderList: keyBy=\"${String(keyBy)}\" but the field is undefined in ${JSON.stringify(item)}`,\n );\n }\n return index;\n }\n\n return value as string | number;\n}\n"]}
@@ -0,0 +1,206 @@
1
+ import * as react from 'react';
2
+ import react__default, { ReactNode, ElementType, ReactElement } from 'react';
3
+
4
+ type RenderFn<T> = (item: T, index: number, items: readonly T[]) => ReactNode;
5
+ interface RenderComponentConfig<P> {
6
+ component: ElementType<P & {
7
+ children?: ReactNode;
8
+ }>;
9
+ /**
10
+ * Specifies the name of the data field
11
+ * @example dataKey="user" will pass item as user prop to the component
12
+ */
13
+ dataKey?: string;
14
+ /** Additional props to pass to the component */
15
+ props?: P;
16
+ }
17
+ type RenderProp<T> = RenderFn<T> | RenderComponentConfig<any>;
18
+ type KeyBy<T> = ((item: T, index: number, items: readonly T[]) => string | number) | keyof T;
19
+ interface RenderListProps<T> {
20
+ items: readonly T[];
21
+ /**
22
+ * Unified render prop
23
+ *
24
+ * @example Function
25
+ * render={(user) => <UserCard user={user} />}
26
+ *
27
+ * @example Component
28
+ * render={{
29
+ * component: UserCard,
30
+ * dataKey: "user",
31
+ * props: { variant: 'compact' }
32
+ * }}
33
+ */
34
+ render: RenderProp<T>;
35
+ /**
36
+ * Specifies how to get the unique key for each item
37
+ * - Pass a function: (item) => item.id
38
+ * - Pass a field name: "id"
39
+ * @default (item, index) => index
40
+ */
41
+ keyBy?: KeyBy<T>;
42
+ }
43
+
44
+ /**
45
+ * RenderList - Recommended list component
46
+ *
47
+ * @example Function style
48
+ * ```tsx
49
+ * <RenderList
50
+ * items={users}
51
+ * render={(user) => <UserCard user={user} />}
52
+ * keyBy="id"
53
+ * />
54
+ * ```
55
+ *
56
+ * @example Component style
57
+ * ```tsx
58
+ * <RenderList
59
+ * items={users}
60
+ * render={{
61
+ * component: UserCard,
62
+ * dataKey: "user",
63
+ * props: { variant: 'compact' }
64
+ * }}
65
+ * keyBy="id"
66
+ * />
67
+ * ```
68
+ */
69
+ declare function RenderList<T>({ items, render, keyBy }: RenderListProps<T>): react__default.JSX.Element;
70
+
71
+ interface RenderIfProps {
72
+ /**
73
+ * Condition to determine which child to render
74
+ * - true: renders the first child
75
+ * - false: renders the second child (if provided)
76
+ */
77
+ when: boolean;
78
+ /**
79
+ * Children to render based on condition
80
+ * - 1 child: renders when `when` is true, nothing when false
81
+ * - 2 children: first renders when `when` is true, second when false
82
+ */
83
+ children: ReactNode;
84
+ }
85
+
86
+ /**
87
+ * RenderIf - Conditional rendering component
88
+ *
89
+ * @example Single child (render when true, nothing when false)
90
+ * ```tsx
91
+ * <RenderIf when={isLoggedIn}>
92
+ * <Dashboard />
93
+ * </RenderIf>
94
+ * ```
95
+ *
96
+ * @example Two children (if/else pattern)
97
+ * ```tsx
98
+ * <RenderIf when={isLoggedIn}>
99
+ * <Dashboard />
100
+ * <Login />
101
+ * </RenderIf>
102
+ * ```
103
+ */
104
+ declare function RenderIf({ when, children }: RenderIfProps): string | number | react.ReactElement<any, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null;
105
+
106
+ type MatchValue = string | readonly string[];
107
+ interface RenderMatchProps<T = string> {
108
+ /**
109
+ * The value to match against items
110
+ * @example "pending" | "success" | "error"
111
+ */
112
+ value: T;
113
+ /**
114
+ * List of match values
115
+ * - Can be a single string: "pending"
116
+ * - Can be an array of strings: ["pending", "processing"]
117
+ *
118
+ * The index of the matched item determines which child to render
119
+ */
120
+ items: readonly MatchValue[];
121
+ /**
122
+ * Children to render based on matched index
123
+ * - First child renders when value matches items[0]
124
+ * - Second child renders when value matches items[1]
125
+ * - And so on...
126
+ */
127
+ children: ReactNode;
128
+ }
129
+
130
+ /**
131
+ * RenderMatch - Match value against items and render corresponding child
132
+ *
133
+ * @example Basic usage
134
+ * ```tsx
135
+ * <RenderMatch value="success" items={['pending', 'success', 'error']}>
136
+ * <PendingState />
137
+ * <SuccessState />
138
+ * <ErrorState />
139
+ * </RenderMatch>
140
+ * ```
141
+ *
142
+ * @example With array values (multiple values map to same child)
143
+ * ```tsx
144
+ * <RenderMatch
145
+ * value="processing"
146
+ * items={[['pending', 'processing'], 'success', 'error']}
147
+ * >
148
+ * <LoadingState />
149
+ * <SuccessState />
150
+ * <ErrorState />
151
+ * </RenderMatch>
152
+ * ```
153
+ */
154
+ declare function RenderMatch<T extends string = string>({ value, items, children, }: RenderMatchProps<T>): ReactElement<any, string | react.JSXElementConstructor<any>> | null;
155
+
156
+ interface RenderSwitchProps {
157
+ /**
158
+ * Array of boolean conditions to match against children
159
+ * - The first true condition determines which child to render
160
+ * @example [true, false, false] renders the first child
161
+ * @example [false, true, false] renders the second child
162
+ */
163
+ cases: readonly boolean[];
164
+ /**
165
+ * Children to render based on matching case
166
+ * - First child renders when cases[0] is true
167
+ * - Second child renders when cases[1] is true
168
+ * - And so on...
169
+ */
170
+ children: ReactNode;
171
+ /**
172
+ * Fallback content to render when no cases match
173
+ * @default null
174
+ */
175
+ fallback?: ReactNode;
176
+ }
177
+
178
+ /**
179
+ * RenderSwitch - Switch-style conditional rendering
180
+ *
181
+ * Renders the first child whose corresponding case condition is true.
182
+ * If none matches, renders `fallback` (or null if not provided).
183
+ *
184
+ * @example Basic usage
185
+ * ```tsx
186
+ * <RenderSwitch cases={[isLoading, isError, isSuccess]}>
187
+ * <LoadingSpinner />
188
+ * <ErrorDisplay />
189
+ * <SuccessMessage />
190
+ * </RenderSwitch>
191
+ * ```
192
+ *
193
+ * @example With fallback
194
+ * ```tsx
195
+ * <RenderSwitch
196
+ * cases={[isAdmin, isModerator]}
197
+ * fallback={<AccessDenied />}
198
+ * >
199
+ * <AdminPanel />
200
+ * <ModeratorPanel />
201
+ * </RenderSwitch>
202
+ * ```
203
+ */
204
+ declare function RenderSwitch({ cases, children, fallback, }: RenderSwitchProps): string | number | boolean | ReactElement<any, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null;
205
+
206
+ export { type KeyBy, type MatchValue, type RenderComponentConfig, type RenderFn, RenderIf, type RenderIfProps, RenderList, type RenderListProps, RenderMatch, type RenderMatchProps, type RenderProp, RenderSwitch, type RenderSwitchProps };
package/dist/main.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ import * as react from 'react';
2
+ import react__default, { ReactNode, ElementType, ReactElement } from 'react';
3
+
4
+ type RenderFn<T> = (item: T, index: number, items: readonly T[]) => ReactNode;
5
+ interface RenderComponentConfig<P> {
6
+ component: ElementType<P & {
7
+ children?: ReactNode;
8
+ }>;
9
+ /**
10
+ * Specifies the name of the data field
11
+ * @example dataKey="user" will pass item as user prop to the component
12
+ */
13
+ dataKey?: string;
14
+ /** Additional props to pass to the component */
15
+ props?: P;
16
+ }
17
+ type RenderProp<T> = RenderFn<T> | RenderComponentConfig<any>;
18
+ type KeyBy<T> = ((item: T, index: number, items: readonly T[]) => string | number) | keyof T;
19
+ interface RenderListProps<T> {
20
+ items: readonly T[];
21
+ /**
22
+ * Unified render prop
23
+ *
24
+ * @example Function
25
+ * render={(user) => <UserCard user={user} />}
26
+ *
27
+ * @example Component
28
+ * render={{
29
+ * component: UserCard,
30
+ * dataKey: "user",
31
+ * props: { variant: 'compact' }
32
+ * }}
33
+ */
34
+ render: RenderProp<T>;
35
+ /**
36
+ * Specifies how to get the unique key for each item
37
+ * - Pass a function: (item) => item.id
38
+ * - Pass a field name: "id"
39
+ * @default (item, index) => index
40
+ */
41
+ keyBy?: KeyBy<T>;
42
+ }
43
+
44
+ /**
45
+ * RenderList - Recommended list component
46
+ *
47
+ * @example Function style
48
+ * ```tsx
49
+ * <RenderList
50
+ * items={users}
51
+ * render={(user) => <UserCard user={user} />}
52
+ * keyBy="id"
53
+ * />
54
+ * ```
55
+ *
56
+ * @example Component style
57
+ * ```tsx
58
+ * <RenderList
59
+ * items={users}
60
+ * render={{
61
+ * component: UserCard,
62
+ * dataKey: "user",
63
+ * props: { variant: 'compact' }
64
+ * }}
65
+ * keyBy="id"
66
+ * />
67
+ * ```
68
+ */
69
+ declare function RenderList<T>({ items, render, keyBy }: RenderListProps<T>): react__default.JSX.Element;
70
+
71
+ interface RenderIfProps {
72
+ /**
73
+ * Condition to determine which child to render
74
+ * - true: renders the first child
75
+ * - false: renders the second child (if provided)
76
+ */
77
+ when: boolean;
78
+ /**
79
+ * Children to render based on condition
80
+ * - 1 child: renders when `when` is true, nothing when false
81
+ * - 2 children: first renders when `when` is true, second when false
82
+ */
83
+ children: ReactNode;
84
+ }
85
+
86
+ /**
87
+ * RenderIf - Conditional rendering component
88
+ *
89
+ * @example Single child (render when true, nothing when false)
90
+ * ```tsx
91
+ * <RenderIf when={isLoggedIn}>
92
+ * <Dashboard />
93
+ * </RenderIf>
94
+ * ```
95
+ *
96
+ * @example Two children (if/else pattern)
97
+ * ```tsx
98
+ * <RenderIf when={isLoggedIn}>
99
+ * <Dashboard />
100
+ * <Login />
101
+ * </RenderIf>
102
+ * ```
103
+ */
104
+ declare function RenderIf({ when, children }: RenderIfProps): string | number | react.ReactElement<any, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null;
105
+
106
+ type MatchValue = string | readonly string[];
107
+ interface RenderMatchProps<T = string> {
108
+ /**
109
+ * The value to match against items
110
+ * @example "pending" | "success" | "error"
111
+ */
112
+ value: T;
113
+ /**
114
+ * List of match values
115
+ * - Can be a single string: "pending"
116
+ * - Can be an array of strings: ["pending", "processing"]
117
+ *
118
+ * The index of the matched item determines which child to render
119
+ */
120
+ items: readonly MatchValue[];
121
+ /**
122
+ * Children to render based on matched index
123
+ * - First child renders when value matches items[0]
124
+ * - Second child renders when value matches items[1]
125
+ * - And so on...
126
+ */
127
+ children: ReactNode;
128
+ }
129
+
130
+ /**
131
+ * RenderMatch - Match value against items and render corresponding child
132
+ *
133
+ * @example Basic usage
134
+ * ```tsx
135
+ * <RenderMatch value="success" items={['pending', 'success', 'error']}>
136
+ * <PendingState />
137
+ * <SuccessState />
138
+ * <ErrorState />
139
+ * </RenderMatch>
140
+ * ```
141
+ *
142
+ * @example With array values (multiple values map to same child)
143
+ * ```tsx
144
+ * <RenderMatch
145
+ * value="processing"
146
+ * items={[['pending', 'processing'], 'success', 'error']}
147
+ * >
148
+ * <LoadingState />
149
+ * <SuccessState />
150
+ * <ErrorState />
151
+ * </RenderMatch>
152
+ * ```
153
+ */
154
+ declare function RenderMatch<T extends string = string>({ value, items, children, }: RenderMatchProps<T>): ReactElement<any, string | react.JSXElementConstructor<any>> | null;
155
+
156
+ interface RenderSwitchProps {
157
+ /**
158
+ * Array of boolean conditions to match against children
159
+ * - The first true condition determines which child to render
160
+ * @example [true, false, false] renders the first child
161
+ * @example [false, true, false] renders the second child
162
+ */
163
+ cases: readonly boolean[];
164
+ /**
165
+ * Children to render based on matching case
166
+ * - First child renders when cases[0] is true
167
+ * - Second child renders when cases[1] is true
168
+ * - And so on...
169
+ */
170
+ children: ReactNode;
171
+ /**
172
+ * Fallback content to render when no cases match
173
+ * @default null
174
+ */
175
+ fallback?: ReactNode;
176
+ }
177
+
178
+ /**
179
+ * RenderSwitch - Switch-style conditional rendering
180
+ *
181
+ * Renders the first child whose corresponding case condition is true.
182
+ * If none matches, renders `fallback` (or null if not provided).
183
+ *
184
+ * @example Basic usage
185
+ * ```tsx
186
+ * <RenderSwitch cases={[isLoading, isError, isSuccess]}>
187
+ * <LoadingSpinner />
188
+ * <ErrorDisplay />
189
+ * <SuccessMessage />
190
+ * </RenderSwitch>
191
+ * ```
192
+ *
193
+ * @example With fallback
194
+ * ```tsx
195
+ * <RenderSwitch
196
+ * cases={[isAdmin, isModerator]}
197
+ * fallback={<AccessDenied />}
198
+ * >
199
+ * <AdminPanel />
200
+ * <ModeratorPanel />
201
+ * </RenderSwitch>
202
+ * ```
203
+ */
204
+ declare function RenderSwitch({ cases, children, fallback, }: RenderSwitchProps): string | number | boolean | ReactElement<any, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null;
205
+
206
+ export { type KeyBy, type MatchValue, type RenderComponentConfig, type RenderFn, RenderIf, type RenderIfProps, RenderList, type RenderListProps, RenderMatch, type RenderMatchProps, type RenderProp, RenderSwitch, type RenderSwitchProps };
@@ -0,0 +1,2 @@
1
+ var c=Object.defineProperty,A=Object.defineProperties;var M=Object.getOwnPropertyDescriptors;var d=Object.getOwnPropertySymbols;var V=Object.prototype.hasOwnProperty,b=Object.prototype.propertyIsEnumerable;var s=(t,e,n)=>e in t?c(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,p=(t,e)=>{for(var n in e||(e={}))V.call(e,n)&&s(t,n,e[n]);if(d)for(var n of d(e))b.call(e,n)&&s(t,n,e[n]);return t},m=(t,e)=>A(t,M(e)),o=(t,e)=>c(t,"name",{value:e,configurable:!0});import u from"react";var h,l=typeof process!="undefined"&&((h=process.env)==null?void 0:h.NODE_ENV)!=="production";function g(t){return typeof t=="function"}o(g,"isRenderFn");function x(t,e,n,r){if(r===void 0)return e;if(typeof r=="function")return r(t,e,n);let i=t[r];return i==null?(l&&console.warn(`RenderList: keyBy="${String(r)}" but the field is undefined in ${JSON.stringify(t)}`),e):i}o(x,"getKey");function R({items:t,render:e,keyBy:n}){return u.createElement(u.Fragment,null,t.map((r,i)=>{let f=x(r,i,t,n);if(g(e))return u.createElement(u.Fragment,{key:f},e(r,i,t));let{component:a,dataKey:w="item",props:C={}}=e,I=m(p({},C),{[w]:r});return u.createElement(u.Fragment,{key:f},u.createElement(a,I))}))}o(R,"RenderList");import{Children as D}from"react";function F({when:t,children:e}){let n=D.toArray(e);return n.length===0?null:(n.length>2&&l&&console.warn(`RenderIf: Expected at most 2 children, but got ${n.length}. Only the first 2 children will be used.`),n.length===1?t?n[0]:null:t?n[0]:n[1])}o(F,"RenderIf");var S=F;import{Children as N,isValidElement as K}from"react";function y(t,e){for(let n=0;n<e.length;n++){let r=e[n];if(typeof r=="string"){if(r===t)return n}else if(r.includes(t))return n}return-1}o(y,"findMatchIndex");function v(t){let e=[];for(let n of t)typeof n=="string"?e.push(n):e.push(...n);return e}o(v,"getAllValues");function L({value:t,items:e,children:n}){var f;let r=y(t,e);if(r===-1){if(l){let a=v(e);console.warn(`RenderMatch: Value "${t}" not found in any of the items. Available values: [${a.join(", ")}]`)}return null}let i=N.toArray(n).filter(a=>K(a));return r>=i.length?(l&&console.warn(`RenderMatch: Not enough children provided. Expected at least ${r+1}, but got ${i.length}.`),null):(f=i[r])!=null?f:null}o(L,"RenderMatch");var O=L;import{Children as T,isValidElement as j}from"react";function E(t){for(let e=0;e<t.length;e++)if(t[e])return e;return-1}o(E,"findTrueCaseIndex");function $(t,e,n){l&&t>e&&console.warn(`${n}: More cases (${t}) than children (${e}). Extra cases will be ignored.`)}o($,"validateCasesLength");function J({cases:t,children:e,fallback:n=null}){let r=T.toArray(e).filter(f=>j(f));$(t.length,r.length,"RenderSwitch");let i=E(t);return i>=0&&i<r.length?r[i]:n}o(J,"RenderSwitch");var P=J;export{S as RenderIf,R as RenderList,O as RenderMatch,P as RenderSwitch};
2
+ //# sourceMappingURL=main.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/render-list/index.tsx","../src/shared/env.ts","../src/render-list/render-list.utils.ts","../src/render-if/index.tsx","../src/render-match/index.tsx","../src/render-match/render-match.utils.ts","../src/render-switch/index.tsx","../src/render-switch/render-switch.utils.ts"],"sourcesContent":["import React from 'react';\nimport type { RenderListProps } from './render-list.type';\nimport { isRenderFn, getKey } from './render-list.utils';\n\n/**\n * RenderList - Recommended list component\n *\n * @example Function style\n * ```tsx\n * <RenderList\n * items={users}\n * render={(user) => <UserCard user={user} />}\n * keyBy=\"id\"\n * />\n * ```\n *\n * @example Component style\n * ```tsx\n * <RenderList\n * items={users}\n * render={{\n * component: UserCard,\n * dataKey: \"user\",\n * props: { variant: 'compact' }\n * }}\n * keyBy=\"id\"\n * />\n * ```\n */\nexport default function RenderList<T>({ items, render, keyBy }: RenderListProps<T>) {\n return (\n <>\n {items.map((item, index) => {\n const key = getKey(item, index, items, keyBy);\n\n // If render is a function, call it directly\n if (isRenderFn(render)) {\n return <React.Fragment key={key}>{render(item, index, items)}</React.Fragment>;\n }\n\n // If render is component config, wrap props automatically\n const { component: Component, dataKey = 'item', props = {} } = render;\n const componentProps = {\n ...props,\n [dataKey]: item,\n };\n\n return (\n <React.Fragment key={key}>\n <Component {...componentProps} />\n </React.Fragment>\n );\n })}\n </>\n );\n}\n","/**\n * Check if running in development mode\n */\nexport const isDev =\n typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n","import type { RenderProp, RenderFn, KeyBy } from './render-list.type';\nimport { isDev } from '../shared/env';\n\n/**\n * Type guard: check if render is a function type\n */\nexport function isRenderFn<T>(render: RenderProp<T>): render is RenderFn<T> {\n return typeof render === 'function';\n}\n\n/**\n * Get the unique key for a list item\n */\nexport function getKey<T>(\n item: T,\n index: number,\n items: readonly T[],\n keyBy: KeyBy<T> | undefined,\n): string | number {\n if (keyBy === undefined) {\n return index;\n }\n\n if (typeof keyBy === 'function') {\n return keyBy(item, index, items);\n }\n\n const value = (item as Record<string, unknown>)[keyBy as string];\n if (value === undefined || value === null) {\n if (isDev) {\n console.warn(\n `RenderList: keyBy=\"${String(keyBy)}\" but the field is undefined in ${JSON.stringify(item)}`,\n );\n }\n return index;\n }\n\n return value as string | number;\n}\n","import { Children } from 'react';\nimport type { RenderIfProps } from './render-if.type';\nimport { isDev } from '../shared/env';\n\n/**\n * RenderIf - Conditional rendering component\n *\n * @example Single child (render when true, nothing when false)\n * ```tsx\n * <RenderIf when={isLoggedIn}>\n * <Dashboard />\n * </RenderIf>\n * ```\n *\n * @example Two children (if/else pattern)\n * ```tsx\n * <RenderIf when={isLoggedIn}>\n * <Dashboard />\n * <Login />\n * </RenderIf>\n * ```\n */\nexport function RenderIf({ when, children }: RenderIfProps) {\n const childArray = Children.toArray(children);\n\n if (childArray.length === 0) {\n return null;\n }\n\n if (childArray.length > 2) {\n if (isDev) {\n console.warn(\n `RenderIf: Expected at most 2 children, but got ${childArray.length}. Only the first 2 children will be used.`\n );\n }\n }\n\n if (childArray.length === 1) {\n return when ? childArray[0] : null;\n }\n\n // length >= 2: first child for true, second for false\n return when ? childArray[0] : childArray[1];\n}\n\nexport default RenderIf;\n","import { Children, isValidElement, type ReactElement } from 'react';\nimport type { RenderMatchProps } from './render-match.type';\nimport { findMatchIndex, getAllValues } from './render-match.utils';\nimport { isDev } from '../shared/env';\n\n/**\n * RenderMatch - Match value against items and render corresponding child\n *\n * @example Basic usage\n * ```tsx\n * <RenderMatch value=\"success\" items={['pending', 'success', 'error']}>\n * <PendingState />\n * <SuccessState />\n * <ErrorState />\n * </RenderMatch>\n * ```\n *\n * @example With array values (multiple values map to same child)\n * ```tsx\n * <RenderMatch\n * value=\"processing\"\n * items={[['pending', 'processing'], 'success', 'error']}\n * >\n * <LoadingState />\n * <SuccessState />\n * <ErrorState />\n * </RenderMatch>\n * ```\n */\nexport function RenderMatch<T extends string = string>({\n value,\n items,\n children,\n}: RenderMatchProps<T>) {\n // Find the matched slot index\n const matchedIndex = findMatchIndex(value, items);\n\n if (matchedIndex === -1) {\n if (isDev) {\n const allValues = getAllValues(items);\n console.warn(\n `RenderMatch: Value \"${value}\" not found in any of the items. Available values: [${allValues.join(', ')}]`\n );\n }\n return null;\n }\n\n const validChildren = Children.toArray(children).filter(\n (child): child is ReactElement => isValidElement(child)\n );\n\n if (matchedIndex >= validChildren.length) {\n if (isDev) {\n console.warn(\n `RenderMatch: Not enough children provided. Expected at least ${matchedIndex + 1}, but got ${validChildren.length}.`\n );\n }\n return null;\n }\n\n return validChildren[matchedIndex] ?? null;\n}\n\nexport default RenderMatch;\n","import type { MatchValue } from './render-match.type';\n\n/**\n * Find the index of the item that matches the value\n * @returns The matched index, or -1 if not found\n */\nexport function findMatchIndex(\n value: string,\n items: readonly MatchValue[]\n): number {\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n if (typeof item === 'string') {\n if (item === value) {\n return i;\n }\n } else {\n // item is string[]\n if (item.includes(value)) {\n return i;\n }\n }\n }\n return -1;\n}\n\n/**\n * Get all possible values from items (flatten string arrays)\n */\nexport function getAllValues(items: readonly MatchValue[]): string[] {\n const result: string[] = [];\n for (const item of items) {\n if (typeof item === 'string') {\n result.push(item);\n } else {\n result.push(...item);\n }\n }\n return result;\n}\n","import { Children, isValidElement, type ReactElement } from 'react';\nimport type { RenderSwitchProps } from './render-switch.type';\nimport { findTrueCaseIndex, validateCasesLength } from './render-switch.utils';\n\n/**\n * RenderSwitch - Switch-style conditional rendering\n *\n * Renders the first child whose corresponding case condition is true.\n * If none matches, renders `fallback` (or null if not provided).\n *\n * @example Basic usage\n * ```tsx\n * <RenderSwitch cases={[isLoading, isError, isSuccess]}>\n * <LoadingSpinner />\n * <ErrorDisplay />\n * <SuccessMessage />\n * </RenderSwitch>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RenderSwitch\n * cases={[isAdmin, isModerator]}\n * fallback={<AccessDenied />}\n * >\n * <AdminPanel />\n * <ModeratorPanel />\n * </RenderSwitch>\n * ```\n */\nexport function RenderSwitch({\n cases,\n children,\n fallback = null,\n}: RenderSwitchProps) {\n const childArray = Children.toArray(children).filter(\n (child): child is ReactElement => isValidElement(child)\n );\n\n validateCasesLength(cases.length, childArray.length, 'RenderSwitch');\n\n const matchedIndex = findTrueCaseIndex(cases);\n\n // Check if the matched index is within the children bounds\n if (matchedIndex >= 0 && matchedIndex < childArray.length) {\n return childArray[matchedIndex];\n }\n\n return fallback;\n}\n\nexport default RenderSwitch;\n","import { isDev } from '../shared/env';\n\n/**\n * Find the index of the first true case\n * @returns The index of the first true case, or -1 if none are true\n */\nexport function findTrueCaseIndex(cases: readonly boolean[]): number {\n for (let i = 0; i < cases.length; i++) {\n if (cases[i]) {\n return i;\n }\n }\n return -1;\n}\n\n/**\n * Validate that the number of cases matches the number of children\n */\nexport function validateCasesLength(\n casesLength: number,\n childrenLength: number,\n componentName: string\n): void {\n if (isDev && casesLength > childrenLength) {\n console.warn(\n `${componentName}: More cases (${casesLength}) than children (${childrenLength}). Extra cases will be ignored.`\n );\n }\n}\n"],"mappings":"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","names":["React","_a","isDev","process","env","NODE_ENV","isRenderFn","render","getKey","item","index","items","keyBy","undefined","value","isDev","console","warn","String","JSON","stringify","RenderList","items","render","keyBy","React","map","item","index","key","getKey","isRenderFn","Fragment","component","Component","dataKey","props","componentProps","__spreadProps","__spreadValues","Children","RenderIf","when","children","childArray","Children","toArray","length","isDev","console","warn","render_if_default","Children","isValidElement","findMatchIndex","value","items","i","length","item","includes","getAllValues","result","push","RenderMatch","value","items","children","_a","matchedIndex","findMatchIndex","isDev","allValues","getAllValues","console","warn","join","validChildren","Children","toArray","filter","child","isValidElement","length","render_match_default","Children","isValidElement","findTrueCaseIndex","cases","i","length","validateCasesLength","casesLength","childrenLength","componentName","isDev","console","warn","RenderSwitch","cases","children","fallback","childArray","Children","toArray","filter","child","isValidElement","validateCasesLength","length","matchedIndex","findTrueCaseIndex","render_switch_default"]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@jswork/react-render-controls",
3
+ "version": "1.0.1",
4
+ "main": "dist/main.cjs.js",
5
+ "module": "dist/main.esm.js",
6
+ "types": "dist/main.d.ts",
7
+ "description": "A lightweight, headless React component library for declarative conditional rendering, pattern matching, and list mapping. SSR-friendly, zero UI, and fully type-safe.",
8
+ "homepage": "https://js.work",
9
+ "license": "MIT",
10
+ "files": [
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "release": "release-it"
17
+ },
18
+ "devDependencies": {
19
+ "@swc/core": "^1.3.93",
20
+ "@types/react": "^18.2.28",
21
+ "@types/react-dom": "^18.2.13",
22
+ "autoprefixer": "^10.4.16",
23
+ "classnames": "^2.5.1",
24
+ "cssnano": "^6.0.1",
25
+ "react": "^18.2.0",
26
+ "react-dom": "^18.2.0",
27
+ "tsup": "^8.2.4",
28
+ "typescript": "^5.2.2"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "*",
32
+ "react-dom": "*",
33
+ "classnames": "*"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org"
38
+ }
39
+ }
@@ -0,0 +1,11 @@
1
+ declare namespace NodeJS {
2
+ interface ProcessEnv {
3
+ readonly NODE_ENV?: string;
4
+ }
5
+
6
+ interface Process {
7
+ env?: ProcessEnv;
8
+ }
9
+ }
10
+
11
+ declare const process: NodeJS.Process;
package/src/main.tsx ADDED
@@ -0,0 +1,14 @@
1
+ export { default as RenderList } from './render-list';
2
+ export { default as RenderIf } from './render-if';
3
+ export { default as RenderMatch } from './render-match';
4
+ export { default as RenderSwitch } from './render-switch';
5
+ export type {
6
+ RenderFn,
7
+ RenderComponentConfig,
8
+ RenderProp,
9
+ KeyBy,
10
+ RenderListProps,
11
+ } from './render-list/render-list.type';
12
+ export type { RenderIfProps } from './render-if/render-if.type';
13
+ export type { RenderMatchProps, MatchValue } from './render-match/render-match.type';
14
+ export type { RenderSwitchProps } from './render-switch/render-switch.type';
@@ -0,0 +1,46 @@
1
+ import { Children } from 'react';
2
+ import type { RenderIfProps } from './render-if.type';
3
+ import { isDev } from '../shared/env';
4
+
5
+ /**
6
+ * RenderIf - Conditional rendering component
7
+ *
8
+ * @example Single child (render when true, nothing when false)
9
+ * ```tsx
10
+ * <RenderIf when={isLoggedIn}>
11
+ * <Dashboard />
12
+ * </RenderIf>
13
+ * ```
14
+ *
15
+ * @example Two children (if/else pattern)
16
+ * ```tsx
17
+ * <RenderIf when={isLoggedIn}>
18
+ * <Dashboard />
19
+ * <Login />
20
+ * </RenderIf>
21
+ * ```
22
+ */
23
+ export function RenderIf({ when, children }: RenderIfProps) {
24
+ const childArray = Children.toArray(children);
25
+
26
+ if (childArray.length === 0) {
27
+ return null;
28
+ }
29
+
30
+ if (childArray.length > 2) {
31
+ if (isDev) {
32
+ console.warn(
33
+ `RenderIf: Expected at most 2 children, but got ${childArray.length}. Only the first 2 children will be used.`
34
+ );
35
+ }
36
+ }
37
+
38
+ if (childArray.length === 1) {
39
+ return when ? childArray[0] : null;
40
+ }
41
+
42
+ // length >= 2: first child for true, second for false
43
+ return when ? childArray[0] : childArray[1];
44
+ }
45
+
46
+ export default RenderIf;
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from 'react';
2
+
3
+ export interface RenderIfProps {
4
+ /**
5
+ * Condition to determine which child to render
6
+ * - true: renders the first child
7
+ * - false: renders the second child (if provided)
8
+ */
9
+ when: boolean;
10
+ /**
11
+ * Children to render based on condition
12
+ * - 1 child: renders when `when` is true, nothing when false
13
+ * - 2 children: first renders when `when` is true, second when false
14
+ */
15
+ children: ReactNode;
16
+ }
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import type { RenderListProps } from './render-list.type';
3
+ import { isRenderFn, getKey } from './render-list.utils';
4
+
5
+ /**
6
+ * RenderList - Recommended list component
7
+ *
8
+ * @example Function style
9
+ * ```tsx
10
+ * <RenderList
11
+ * items={users}
12
+ * render={(user) => <UserCard user={user} />}
13
+ * keyBy="id"
14
+ * />
15
+ * ```
16
+ *
17
+ * @example Component style
18
+ * ```tsx
19
+ * <RenderList
20
+ * items={users}
21
+ * render={{
22
+ * component: UserCard,
23
+ * dataKey: "user",
24
+ * props: { variant: 'compact' }
25
+ * }}
26
+ * keyBy="id"
27
+ * />
28
+ * ```
29
+ */
30
+ export default function RenderList<T>({ items, render, keyBy }: RenderListProps<T>) {
31
+ return (
32
+ <>
33
+ {items.map((item, index) => {
34
+ const key = getKey(item, index, items, keyBy);
35
+
36
+ // If render is a function, call it directly
37
+ if (isRenderFn(render)) {
38
+ return <React.Fragment key={key}>{render(item, index, items)}</React.Fragment>;
39
+ }
40
+
41
+ // If render is component config, wrap props automatically
42
+ const { component: Component, dataKey = 'item', props = {} } = render;
43
+ const componentProps = {
44
+ ...props,
45
+ [dataKey]: item,
46
+ };
47
+
48
+ return (
49
+ <React.Fragment key={key}>
50
+ <Component {...componentProps} />
51
+ </React.Fragment>
52
+ );
53
+ })}
54
+ </>
55
+ );
56
+ }
@@ -0,0 +1,43 @@
1
+ import { type ElementType, type ReactNode } from 'react';
2
+
3
+ export type RenderFn<T> = (item: T, index: number, items: readonly T[]) => ReactNode;
4
+
5
+ export interface RenderComponentConfig<P> {
6
+ component: ElementType<P & { children?: ReactNode }>;
7
+ /**
8
+ * Specifies the name of the data field
9
+ * @example dataKey="user" will pass item as user prop to the component
10
+ */
11
+ dataKey?: string;
12
+ /** Additional props to pass to the component */
13
+ props?: P;
14
+ }
15
+
16
+ export type RenderProp<T> = RenderFn<T> | RenderComponentConfig<any>;
17
+
18
+ export type KeyBy<T> = ((item: T, index: number, items: readonly T[]) => string | number) | keyof T;
19
+
20
+ export interface RenderListProps<T> {
21
+ items: readonly T[];
22
+ /**
23
+ * Unified render prop
24
+ *
25
+ * @example Function
26
+ * render={(user) => <UserCard user={user} />}
27
+ *
28
+ * @example Component
29
+ * render={{
30
+ * component: UserCard,
31
+ * dataKey: "user",
32
+ * props: { variant: 'compact' }
33
+ * }}
34
+ */
35
+ render: RenderProp<T>;
36
+ /**
37
+ * Specifies how to get the unique key for each item
38
+ * - Pass a function: (item) => item.id
39
+ * - Pass a field name: "id"
40
+ * @default (item, index) => index
41
+ */
42
+ keyBy?: KeyBy<T>;
43
+ }
@@ -0,0 +1,39 @@
1
+ import type { RenderProp, RenderFn, KeyBy } from './render-list.type';
2
+ import { isDev } from '../shared/env';
3
+
4
+ /**
5
+ * Type guard: check if render is a function type
6
+ */
7
+ export function isRenderFn<T>(render: RenderProp<T>): render is RenderFn<T> {
8
+ return typeof render === 'function';
9
+ }
10
+
11
+ /**
12
+ * Get the unique key for a list item
13
+ */
14
+ export function getKey<T>(
15
+ item: T,
16
+ index: number,
17
+ items: readonly T[],
18
+ keyBy: KeyBy<T> | undefined,
19
+ ): string | number {
20
+ if (keyBy === undefined) {
21
+ return index;
22
+ }
23
+
24
+ if (typeof keyBy === 'function') {
25
+ return keyBy(item, index, items);
26
+ }
27
+
28
+ const value = (item as Record<string, unknown>)[keyBy as string];
29
+ if (value === undefined || value === null) {
30
+ if (isDev) {
31
+ console.warn(
32
+ `RenderList: keyBy="${String(keyBy)}" but the field is undefined in ${JSON.stringify(item)}`,
33
+ );
34
+ }
35
+ return index;
36
+ }
37
+
38
+ return value as string | number;
39
+ }
@@ -0,0 +1,64 @@
1
+ import { Children, isValidElement, type ReactElement } from 'react';
2
+ import type { RenderMatchProps } from './render-match.type';
3
+ import { findMatchIndex, getAllValues } from './render-match.utils';
4
+ import { isDev } from '../shared/env';
5
+
6
+ /**
7
+ * RenderMatch - Match value against items and render corresponding child
8
+ *
9
+ * @example Basic usage
10
+ * ```tsx
11
+ * <RenderMatch value="success" items={['pending', 'success', 'error']}>
12
+ * <PendingState />
13
+ * <SuccessState />
14
+ * <ErrorState />
15
+ * </RenderMatch>
16
+ * ```
17
+ *
18
+ * @example With array values (multiple values map to same child)
19
+ * ```tsx
20
+ * <RenderMatch
21
+ * value="processing"
22
+ * items={[['pending', 'processing'], 'success', 'error']}
23
+ * >
24
+ * <LoadingState />
25
+ * <SuccessState />
26
+ * <ErrorState />
27
+ * </RenderMatch>
28
+ * ```
29
+ */
30
+ export function RenderMatch<T extends string = string>({
31
+ value,
32
+ items,
33
+ children,
34
+ }: RenderMatchProps<T>) {
35
+ // Find the matched slot index
36
+ const matchedIndex = findMatchIndex(value, items);
37
+
38
+ if (matchedIndex === -1) {
39
+ if (isDev) {
40
+ const allValues = getAllValues(items);
41
+ console.warn(
42
+ `RenderMatch: Value "${value}" not found in any of the items. Available values: [${allValues.join(', ')}]`
43
+ );
44
+ }
45
+ return null;
46
+ }
47
+
48
+ const validChildren = Children.toArray(children).filter(
49
+ (child): child is ReactElement => isValidElement(child)
50
+ );
51
+
52
+ if (matchedIndex >= validChildren.length) {
53
+ if (isDev) {
54
+ console.warn(
55
+ `RenderMatch: Not enough children provided. Expected at least ${matchedIndex + 1}, but got ${validChildren.length}.`
56
+ );
57
+ }
58
+ return null;
59
+ }
60
+
61
+ return validChildren[matchedIndex] ?? null;
62
+ }
63
+
64
+ export default RenderMatch;
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from 'react';
2
+
3
+ export type MatchValue = string | readonly string[];
4
+
5
+ export interface RenderMatchProps<T = string> {
6
+ /**
7
+ * The value to match against items
8
+ * @example "pending" | "success" | "error"
9
+ */
10
+ value: T;
11
+ /**
12
+ * List of match values
13
+ * - Can be a single string: "pending"
14
+ * - Can be an array of strings: ["pending", "processing"]
15
+ *
16
+ * The index of the matched item determines which child to render
17
+ */
18
+ items: readonly MatchValue[];
19
+ /**
20
+ * Children to render based on matched index
21
+ * - First child renders when value matches items[0]
22
+ * - Second child renders when value matches items[1]
23
+ * - And so on...
24
+ */
25
+ children: ReactNode;
26
+ }
@@ -0,0 +1,40 @@
1
+ import type { MatchValue } from './render-match.type';
2
+
3
+ /**
4
+ * Find the index of the item that matches the value
5
+ * @returns The matched index, or -1 if not found
6
+ */
7
+ export function findMatchIndex(
8
+ value: string,
9
+ items: readonly MatchValue[]
10
+ ): number {
11
+ for (let i = 0; i < items.length; i++) {
12
+ const item = items[i];
13
+ if (typeof item === 'string') {
14
+ if (item === value) {
15
+ return i;
16
+ }
17
+ } else {
18
+ // item is string[]
19
+ if (item.includes(value)) {
20
+ return i;
21
+ }
22
+ }
23
+ }
24
+ return -1;
25
+ }
26
+
27
+ /**
28
+ * Get all possible values from items (flatten string arrays)
29
+ */
30
+ export function getAllValues(items: readonly MatchValue[]): string[] {
31
+ const result: string[] = [];
32
+ for (const item of items) {
33
+ if (typeof item === 'string') {
34
+ result.push(item);
35
+ } else {
36
+ result.push(...item);
37
+ }
38
+ }
39
+ return result;
40
+ }
@@ -0,0 +1,52 @@
1
+ import { Children, isValidElement, type ReactElement } from 'react';
2
+ import type { RenderSwitchProps } from './render-switch.type';
3
+ import { findTrueCaseIndex, validateCasesLength } from './render-switch.utils';
4
+
5
+ /**
6
+ * RenderSwitch - Switch-style conditional rendering
7
+ *
8
+ * Renders the first child whose corresponding case condition is true.
9
+ * If none matches, renders `fallback` (or null if not provided).
10
+ *
11
+ * @example Basic usage
12
+ * ```tsx
13
+ * <RenderSwitch cases={[isLoading, isError, isSuccess]}>
14
+ * <LoadingSpinner />
15
+ * <ErrorDisplay />
16
+ * <SuccessMessage />
17
+ * </RenderSwitch>
18
+ * ```
19
+ *
20
+ * @example With fallback
21
+ * ```tsx
22
+ * <RenderSwitch
23
+ * cases={[isAdmin, isModerator]}
24
+ * fallback={<AccessDenied />}
25
+ * >
26
+ * <AdminPanel />
27
+ * <ModeratorPanel />
28
+ * </RenderSwitch>
29
+ * ```
30
+ */
31
+ export function RenderSwitch({
32
+ cases,
33
+ children,
34
+ fallback = null,
35
+ }: RenderSwitchProps) {
36
+ const childArray = Children.toArray(children).filter(
37
+ (child): child is ReactElement => isValidElement(child)
38
+ );
39
+
40
+ validateCasesLength(cases.length, childArray.length, 'RenderSwitch');
41
+
42
+ const matchedIndex = findTrueCaseIndex(cases);
43
+
44
+ // Check if the matched index is within the children bounds
45
+ if (matchedIndex >= 0 && matchedIndex < childArray.length) {
46
+ return childArray[matchedIndex];
47
+ }
48
+
49
+ return fallback;
50
+ }
51
+
52
+ export default RenderSwitch;
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from 'react';
2
+
3
+ export interface RenderSwitchProps {
4
+ /**
5
+ * Array of boolean conditions to match against children
6
+ * - The first true condition determines which child to render
7
+ * @example [true, false, false] renders the first child
8
+ * @example [false, true, false] renders the second child
9
+ */
10
+ cases: readonly boolean[];
11
+ /**
12
+ * Children to render based on matching case
13
+ * - First child renders when cases[0] is true
14
+ * - Second child renders when cases[1] is true
15
+ * - And so on...
16
+ */
17
+ children: ReactNode;
18
+ /**
19
+ * Fallback content to render when no cases match
20
+ * @default null
21
+ */
22
+ fallback?: ReactNode;
23
+ }
@@ -0,0 +1,29 @@
1
+ import { isDev } from '../shared/env';
2
+
3
+ /**
4
+ * Find the index of the first true case
5
+ * @returns The index of the first true case, or -1 if none are true
6
+ */
7
+ export function findTrueCaseIndex(cases: readonly boolean[]): number {
8
+ for (let i = 0; i < cases.length; i++) {
9
+ if (cases[i]) {
10
+ return i;
11
+ }
12
+ }
13
+ return -1;
14
+ }
15
+
16
+ /**
17
+ * Validate that the number of cases matches the number of children
18
+ */
19
+ export function validateCasesLength(
20
+ casesLength: number,
21
+ childrenLength: number,
22
+ componentName: string
23
+ ): void {
24
+ if (isDev && casesLength > childrenLength) {
25
+ console.warn(
26
+ `${componentName}: More cases (${casesLength}) than children (${childrenLength}). Extra cases will be ignored.`
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Check if running in development mode
3
+ */
4
+ export const isDev =
5
+ typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
6
+
package/src/style.scss ADDED
@@ -0,0 +1,3 @@
1
+ //.react-render-controls {
2
+ // color: red;
3
+ //}