@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 +7 -0
- package/dist/main.cjs.js +2 -0
- package/dist/main.cjs.js.map +1 -0
- package/dist/main.d.mts +206 -0
- package/dist/main.d.ts +206 -0
- package/dist/main.esm.js +2 -0
- package/dist/main.esm.js.map +1 -0
- package/package.json +39 -0
- package/src/global.d.ts +11 -0
- package/src/main.tsx +14 -0
- package/src/render-if/index.tsx +46 -0
- package/src/render-if/render-if.type.ts +16 -0
- package/src/render-list/index.tsx +56 -0
- package/src/render-list/render-list.type.ts +43 -0
- package/src/render-list/render-list.utils.ts +39 -0
- package/src/render-match/index.tsx +64 -0
- package/src/render-match/render-match.type.ts +26 -0
- package/src/render-match/render-match.utils.ts +40 -0
- package/src/render-switch/index.tsx +52 -0
- package/src/render-switch/render-switch.type.ts +23 -0
- package/src/render-switch/render-switch.utils.ts +29 -0
- package/src/shared/env.ts +6 -0
- package/src/style.scss +3 -0
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
|
+
```
|
package/dist/main.cjs.js
ADDED
|
@@ -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"]}
|
package/dist/main.d.mts
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 };
|
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 };
|
package/dist/main.esm.js
ADDED
|
@@ -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
|
+
}
|
package/src/global.d.ts
ADDED
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
|
+
}
|
package/src/style.scss
ADDED