@evanion/react-widget 0.1.0
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/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/constants.d.ts +24 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +116 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/widget.d.ts +27 -0
- package/dist/widget.d.ts.map +1 -0
- package/dist/widgets.d.ts +7 -0
- package/dist/widgets.d.ts.map +1 -0
- package/package.json +62 -0
- package/src/constants.ts +26 -0
- package/src/index.ts +5 -0
- package/src/types.ts +128 -0
- package/src/utils.tsx +66 -0
- package/src/widget.tsx +115 -0
- package/src/widgets.tsx +76 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-08-26)
|
|
4
|
+
|
|
5
|
+
First published release of `@evanion/react-widget`.
|
|
6
|
+
|
|
7
|
+
Still 0.x deliberately: the API works and is type-checked, but has not been
|
|
8
|
+
exercised by real consumers yet, so it may still move.
|
|
9
|
+
|
|
10
|
+
### 🚀 Features
|
|
11
|
+
|
|
12
|
+
- `createWidgets` infers from the component map you give it. An item's `type`
|
|
13
|
+
must be a key of the map, and its `props` are checked against that component,
|
|
14
|
+
so an unknown widget type is a compile error rather than a runtime warning.
|
|
15
|
+
- `defineItems` helper, for item arrays declared in a variable — a bare array
|
|
16
|
+
literal widens `type` to `string` and silently loses the check.
|
|
17
|
+
- Nested widgets via an injected `<Output />`, to any depth.
|
|
18
|
+
- Custom chrome (`wrapper` and `item`), overridable per instance.
|
|
19
|
+
- Built-in error boundary and Suspense fallback per widget.
|
|
20
|
+
|
|
21
|
+
### 🩹 Fixes
|
|
22
|
+
|
|
23
|
+
- Nesting silently stopped at depth 2 — `Output` never passed an `Output` to
|
|
24
|
+
grandchildren, so the third level was dropped.
|
|
25
|
+
- The unknown-type guard used `in`, which walks the prototype chain, so a
|
|
26
|
+
CMS-supplied `type` of `constructor` or `toString` crashed the render instead
|
|
27
|
+
of warning and skipping.
|
|
28
|
+
- Nested subtrees remounted on every parent render, discarding child state,
|
|
29
|
+
effects and focus, because a new `Output` component type was built each time.
|
|
30
|
+
- Nested widgets ignored instance-level `chrome`.
|
|
31
|
+
- `DefaultWrapper` and `DefaultItem` were documented but never exported.
|
|
32
|
+
|
|
33
|
+
### 📦 Packaging
|
|
34
|
+
|
|
35
|
+
- Declared a `react` peer dependency (`^18 || ^19`); there was previously no
|
|
36
|
+
dependency information at all.
|
|
37
|
+
- Added `LICENSE`, `repository`, `engines` and `sideEffects`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mikael Pettersson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# @evanion/react-widget
|
|
2
|
+
|
|
3
|
+
A powerful React library for creating dynamic, reusable widget regions from structured data. Perfect for building CMS-driven layouts, dynamic sidebars, dashboards, and any interface that needs to render different components based on configuration data.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Type-safe**: `createWidgets` infers from your component map, so an unknown
|
|
8
|
+
widget `type` or mismatched `props` is a compile error, not a runtime warning
|
|
9
|
+
- 🔧 **Flexible**: Support for custom chrome components and wrappers
|
|
10
|
+
- ⚡ **Lightweight**: Minimal bundle size with zero dependencies
|
|
11
|
+
- 🎨 **Customizable**: Easy theming and styling through wrapper components
|
|
12
|
+
- 🔄 **Context-aware**: Built-in React Context support for component sharing
|
|
13
|
+
- 📦 **Tree-shakable**: Only import what you need
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @evanion/react-widget
|
|
19
|
+
# or
|
|
20
|
+
yarn add @evanion/react-widget
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @evanion/react-widget
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Typing your items
|
|
26
|
+
|
|
27
|
+
`createWidgets` infers the allowed `type` values and each item's `props` from the
|
|
28
|
+
component map you give it.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
const { Widgets, defineItems } = createWidgets({
|
|
32
|
+
components: { news: NewsTeaser, weather: WeatherCard },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const items = defineItems([
|
|
36
|
+
{ id: '1', type: 'news', props: { title: 'Hello' } },
|
|
37
|
+
{ id: '2', type: 'nope', props: {} }, // ✗ 'nope' is not in the component map
|
|
38
|
+
{ id: '3', type: 'weather', props: { celsius: 'warm' } }, // ✗ celsius is a number
|
|
39
|
+
]);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`defineItems` is an identity function that exists purely to supply the contextual
|
|
43
|
+
type. A bare `const items = [{ type: 'news', ... }]` widens `type` to `string`,
|
|
44
|
+
which cannot narrow to the map's keys, and the check is silently lost. Writing
|
|
45
|
+
the array inline in JSX works too — that is already contextually typed.
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { createWidgets } from '@evanion/react-widget';
|
|
51
|
+
import { PropsWithChildren } from 'react';
|
|
52
|
+
|
|
53
|
+
// Define your widget components
|
|
54
|
+
const NewsTeaser = ({ title, publishedAt, body }: NewsProps) => (
|
|
55
|
+
<article>
|
|
56
|
+
<h3>{title}</h3>
|
|
57
|
+
<time>{publishedAt.toLocaleDateString()}</time>
|
|
58
|
+
<p>{body}</p>
|
|
59
|
+
</article>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const UserSidebar = ({ username, avatar, messages }: UserProps) => (
|
|
63
|
+
<div className="user-info">
|
|
64
|
+
<img src={avatar} alt={username} />
|
|
65
|
+
<span>{username}</span>
|
|
66
|
+
<span>{messages} messages</span>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Create your widget configuration
|
|
71
|
+
const { Widgets } = createWidgets({
|
|
72
|
+
components: {
|
|
73
|
+
news: NewsTeaser,
|
|
74
|
+
userInfo: UserSidebar,
|
|
75
|
+
},
|
|
76
|
+
chrome: {
|
|
77
|
+
wrapper: ({ children }: PropsWithChildren) => (
|
|
78
|
+
<aside className="sidebar">{children}</aside>
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Define your widget data
|
|
84
|
+
const items = [
|
|
85
|
+
{
|
|
86
|
+
id: 'userinfo',
|
|
87
|
+
type: 'userInfo',
|
|
88
|
+
props: {
|
|
89
|
+
username: 'Evanion',
|
|
90
|
+
avatar: 'https://evanion.com/avatar.jpg',
|
|
91
|
+
messages: 5,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'news1',
|
|
96
|
+
type: 'news',
|
|
97
|
+
props: {
|
|
98
|
+
title: 'Breaking News',
|
|
99
|
+
publishedAt: new Date(),
|
|
100
|
+
body: 'This is a sample news article...',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// Use in your layout
|
|
106
|
+
function MyLayout({ children }: PropsWithChildren) {
|
|
107
|
+
return (
|
|
108
|
+
<main>
|
|
109
|
+
<article>{children}</article>
|
|
110
|
+
<Widgets items={items} />
|
|
111
|
+
</main>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `createWidgets<Items>(config)`
|
|
119
|
+
|
|
120
|
+
Creates a widget system with the given configuration.
|
|
121
|
+
|
|
122
|
+
#### Parameters
|
|
123
|
+
|
|
124
|
+
- `config.components` - Object mapping widget types to React components
|
|
125
|
+
- `config.chrome` - Optional wrapper components for styling
|
|
126
|
+
- `config.context` - Optional React Context for component sharing
|
|
127
|
+
|
|
128
|
+
#### Returns
|
|
129
|
+
|
|
130
|
+
- `Widgets` - Component for rendering widget items
|
|
131
|
+
- `WidgetsProvider` - Context provider for sharing components
|
|
132
|
+
- `useWidgets` - Hook for accessing widget components in context
|
|
133
|
+
|
|
134
|
+
### `Widgets` Component
|
|
135
|
+
|
|
136
|
+
Renders a list of widget items.
|
|
137
|
+
|
|
138
|
+
#### Props
|
|
139
|
+
|
|
140
|
+
- `items` - Array of widget items to render
|
|
141
|
+
- `components` - Optional override components for this instance
|
|
142
|
+
- `chrome` - Optional chrome overrides for this instance
|
|
143
|
+
|
|
144
|
+
### Widget Item Structure
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
interface WidgetItem {
|
|
148
|
+
id: string; // Unique identifier
|
|
149
|
+
type: string; // Widget type -- must be a key of your component map
|
|
150
|
+
props: object; // Props for that component, checked against it
|
|
151
|
+
children?: WidgetItem[]; // Nested widgets, rendered by the injected <Output />
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Advanced Usage
|
|
156
|
+
|
|
157
|
+
### Custom Chrome Components
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
const { Widgets } = createWidgets({
|
|
161
|
+
components: {/* your components */},
|
|
162
|
+
chrome: {
|
|
163
|
+
wrapper: ({ children }) => (
|
|
164
|
+
<div className="widget-container">
|
|
165
|
+
<header>My Widgets</header>
|
|
166
|
+
<div className="widget-content">{children}</div>
|
|
167
|
+
</div>
|
|
168
|
+
),
|
|
169
|
+
item: ({ children }) => <div className="widget-item">{children}</div>,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Using Context for Component Sharing
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { createContext } from 'react';
|
|
178
|
+
|
|
179
|
+
const MyWidgetContext = createContext({});
|
|
180
|
+
|
|
181
|
+
const { Widgets, WidgetsProvider } = createWidgets({
|
|
182
|
+
components: {/* your components */},
|
|
183
|
+
context: MyWidgetContext,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Use the context in your app
|
|
187
|
+
function App() {
|
|
188
|
+
return (
|
|
189
|
+
<WidgetsProvider>
|
|
190
|
+
<MyLayout />
|
|
191
|
+
</WidgetsProvider>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Instance-specific Component Overrides
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
function MyPage() {
|
|
200
|
+
const specialNewsComponent = ({ title, ...props }) => (
|
|
201
|
+
<div className="featured-news">
|
|
202
|
+
<h2>Featured: {title}</h2>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return <Widgets items={items} components={{ news: specialNewsComponent }} />;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Nested Widgets
|
|
211
|
+
|
|
212
|
+
Use the `<Output />` component to render nested widgets within your components:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
const { Widgets, Output } = createWidgets({
|
|
216
|
+
components: {
|
|
217
|
+
card: CardWidget,
|
|
218
|
+
text: TextWidget,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Card component that can contain nested widgets
|
|
223
|
+
const CardWidget = ({ title, Output }) => (
|
|
224
|
+
<div className="card">
|
|
225
|
+
<h3>{title}</h3>
|
|
226
|
+
<Output />
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const TextWidget = ({ content }) => <p>{content}</p>;
|
|
231
|
+
|
|
232
|
+
// Usage with nested widgets
|
|
233
|
+
const items = [
|
|
234
|
+
{
|
|
235
|
+
id: 'card1',
|
|
236
|
+
type: 'card',
|
|
237
|
+
props: {
|
|
238
|
+
title: 'My Card',
|
|
239
|
+
},
|
|
240
|
+
children: [
|
|
241
|
+
{
|
|
242
|
+
id: 'text1',
|
|
243
|
+
type: 'text',
|
|
244
|
+
props: { content: 'This is nested content' },
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Error Handling & Performance
|
|
252
|
+
|
|
253
|
+
The library includes built-in error handling and performance optimizations:
|
|
254
|
+
|
|
255
|
+
#### Error Boundaries
|
|
256
|
+
|
|
257
|
+
Each widget is automatically wrapped in an error boundary that:
|
|
258
|
+
|
|
259
|
+
- Catches rendering errors and displays a fallback UI
|
|
260
|
+
- Logs detailed error information to the console
|
|
261
|
+
- Prevents one failing widget from breaking the entire page
|
|
262
|
+
|
|
263
|
+
#### Suspense Support
|
|
264
|
+
|
|
265
|
+
Widgets support React Suspense for loading states:
|
|
266
|
+
|
|
267
|
+
- Automatic loading fallback for lazy-loaded components
|
|
268
|
+
- Customizable loading UI through chrome components
|
|
269
|
+
- Graceful handling of async operations
|
|
270
|
+
|
|
271
|
+
#### Performance Optimizations
|
|
272
|
+
|
|
273
|
+
- `React.memo` for preventing unnecessary re-renders
|
|
274
|
+
- `useCallback` for stable function references
|
|
275
|
+
- Optimized dependency arrays for `useMemo`
|
|
276
|
+
- Silent error handling with console warnings for unknown widget types
|
|
277
|
+
|
|
278
|
+
#### Unknown Widget Handling
|
|
279
|
+
|
|
280
|
+
When a widget type is not found:
|
|
281
|
+
|
|
282
|
+
- Logs a warning with widget type and ID
|
|
283
|
+
- Silently skips rendering (doesn't break the page)
|
|
284
|
+
- Continues rendering other widgets normally
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
// Example: Custom error boundary and loading states
|
|
288
|
+
const CustomItem = ({ children, ...props }) => (
|
|
289
|
+
<div {...props}>
|
|
290
|
+
<ErrorBoundary fallback={<div>Custom error UI</div>}>
|
|
291
|
+
<Suspense fallback={<div>Custom loading...</div>}>{children}</Suspense>
|
|
292
|
+
</ErrorBoundary>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const { Widgets } = createWidgets({
|
|
297
|
+
components: {/* ... */},
|
|
298
|
+
chrome: { item: CustomItem },
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### TypeScript Support
|
|
303
|
+
|
|
304
|
+
The library provides full TypeScript support with type inference:
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
interface NewsProps {
|
|
308
|
+
title: string;
|
|
309
|
+
publishedAt: Date;
|
|
310
|
+
body: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface UserProps {
|
|
314
|
+
username: string;
|
|
315
|
+
avatar: string;
|
|
316
|
+
messages: number;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const { Widgets } = createWidgets<{
|
|
320
|
+
news: NewsProps;
|
|
321
|
+
userInfo: UserProps;
|
|
322
|
+
}>({
|
|
323
|
+
components: {
|
|
324
|
+
news: NewsTeaser,
|
|
325
|
+
userInfo: UserSidebar,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// TypeScript will now enforce correct prop types
|
|
330
|
+
const items = [
|
|
331
|
+
{
|
|
332
|
+
id: 'news1',
|
|
333
|
+
type: 'news',
|
|
334
|
+
props: {
|
|
335
|
+
title: 'Hello World',
|
|
336
|
+
publishedAt: new Date(),
|
|
337
|
+
body: 'Content here',
|
|
338
|
+
// TypeScript will error if any required props are missing
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Use Cases
|
|
345
|
+
|
|
346
|
+
- **CMS-driven layouts**: Render page content based on CMS configuration
|
|
347
|
+
- **Dashboard widgets**: Dynamic dashboard with configurable components
|
|
348
|
+
- **Sidebar content**: Dynamic sidebar with different widget types
|
|
349
|
+
- **Marketing pages**: A/B testing different content layouts
|
|
350
|
+
- **Admin panels**: Configurable admin interface components
|
|
351
|
+
- **E-commerce**: Dynamic product showcases and recommendations
|
|
352
|
+
|
|
353
|
+
## Examples
|
|
354
|
+
|
|
355
|
+
### E-commerce Product Showcase
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
const { Widgets } = createWidgets({
|
|
359
|
+
components: {
|
|
360
|
+
productCard: ProductCard,
|
|
361
|
+
banner: Banner,
|
|
362
|
+
categoryFilter: CategoryFilter,
|
|
363
|
+
},
|
|
364
|
+
chrome: {
|
|
365
|
+
wrapper: ({ children }) => (
|
|
366
|
+
<div className="product-showcase">{children}</div>
|
|
367
|
+
),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const showcaseItems = [
|
|
372
|
+
{
|
|
373
|
+
id: 'banner1',
|
|
374
|
+
type: 'banner',
|
|
375
|
+
props: {
|
|
376
|
+
image: '/banner.jpg',
|
|
377
|
+
title: 'Summer Sale',
|
|
378
|
+
cta: 'Shop Now',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: 'filter1',
|
|
383
|
+
type: 'categoryFilter',
|
|
384
|
+
props: {
|
|
385
|
+
categories: ['Electronics', 'Clothing', 'Books'],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: 'product1',
|
|
390
|
+
type: 'productCard',
|
|
391
|
+
props: {
|
|
392
|
+
name: 'Wireless Headphones',
|
|
393
|
+
price: 99.99,
|
|
394
|
+
image: '/headphones.jpg',
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Blog Layout with Sidebar
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
const { Widgets } = createWidgets({
|
|
404
|
+
components: {
|
|
405
|
+
authorBio: AuthorBio,
|
|
406
|
+
relatedPosts: RelatedPosts,
|
|
407
|
+
newsletterSignup: NewsletterSignup,
|
|
408
|
+
socialShare: SocialShare,
|
|
409
|
+
},
|
|
410
|
+
chrome: {
|
|
411
|
+
wrapper: ({ children }) => (
|
|
412
|
+
<aside className="blog-sidebar">{children}</aside>
|
|
413
|
+
),
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Contributing
|
|
419
|
+
|
|
420
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const ERROR_MESSAGES: {
|
|
2
|
+
readonly UNKNOWN_WIDGET: (type: string, id: string) => string;
|
|
3
|
+
readonly WIDGET_ERROR: (type: string, id: string) => string;
|
|
4
|
+
readonly WIDGET_FAILED: (type: string) => string;
|
|
5
|
+
readonly LOADING: "Loading widget...";
|
|
6
|
+
readonly UNKNOWN: "unknown";
|
|
7
|
+
};
|
|
8
|
+
export declare const DEFAULT_STYLES: {
|
|
9
|
+
readonly ERROR: {
|
|
10
|
+
readonly padding: "8px";
|
|
11
|
+
readonly border: "1px solid #ff6b6b";
|
|
12
|
+
readonly borderRadius: "4px";
|
|
13
|
+
readonly backgroundColor: "#ffe0e0";
|
|
14
|
+
readonly color: "#d63031";
|
|
15
|
+
};
|
|
16
|
+
readonly LOADING: {
|
|
17
|
+
readonly padding: "8px";
|
|
18
|
+
readonly border: "1px solid #ddd";
|
|
19
|
+
readonly borderRadius: "4px";
|
|
20
|
+
readonly backgroundColor: "#f8f9fa";
|
|
21
|
+
readonly color: "#666";
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc;oCACF,MAAM,MAAM,MAAM;kCAEpB,MAAM,MAAM,MAAM;mCAEjB,MAAM;;;CAGpB,CAAC;AAEX,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;CAejB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import e, { Component as t, Suspense as n, createContext as r, memo as i, useContext as a, useMemo as o } from "react";
|
|
2
|
+
import { Fragment as s, jsx as c } from "react/jsx-runtime";
|
|
3
|
+
//#region src/constants.ts
|
|
4
|
+
var l = {
|
|
5
|
+
UNKNOWN_WIDGET: (e, t) => `Unknown widget type "${e}" for widget ID "${t}". Skipping render.`,
|
|
6
|
+
WIDGET_ERROR: (e, t) => `Widget Error: ${e} (ID: ${t})`,
|
|
7
|
+
WIDGET_FAILED: (e) => `Widget failed to render: ${e}`,
|
|
8
|
+
LOADING: "Loading widget...",
|
|
9
|
+
UNKNOWN: "unknown"
|
|
10
|
+
}, u = {
|
|
11
|
+
ERROR: {
|
|
12
|
+
padding: "8px",
|
|
13
|
+
border: "1px solid #ff6b6b",
|
|
14
|
+
borderRadius: "4px",
|
|
15
|
+
backgroundColor: "#ffe0e0",
|
|
16
|
+
color: "#d63031"
|
|
17
|
+
},
|
|
18
|
+
LOADING: {
|
|
19
|
+
padding: "8px",
|
|
20
|
+
border: "1px solid #ddd",
|
|
21
|
+
borderRadius: "4px",
|
|
22
|
+
backgroundColor: "#f8f9fa",
|
|
23
|
+
color: "#666"
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/widgets.tsx
|
|
28
|
+
function d(e) {
|
|
29
|
+
return /* @__PURE__ */ c("section", { ...e });
|
|
30
|
+
}
|
|
31
|
+
var f = class extends t {
|
|
32
|
+
constructor(e) {
|
|
33
|
+
super(e), this.state = { hasError: !1 };
|
|
34
|
+
}
|
|
35
|
+
static getDerivedStateFromError(e) {
|
|
36
|
+
return {
|
|
37
|
+
hasError: !0,
|
|
38
|
+
error: e
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
componentDidCatch(e, t) {
|
|
42
|
+
console.error(l.WIDGET_ERROR(this.props.widgetType, this.props.widgetId), e, t);
|
|
43
|
+
}
|
|
44
|
+
render() {
|
|
45
|
+
return this.state.hasError ? /* @__PURE__ */ c("div", {
|
|
46
|
+
style: u.ERROR,
|
|
47
|
+
children: l.WIDGET_FAILED(this.props.widgetType)
|
|
48
|
+
}) : this.props.children;
|
|
49
|
+
}
|
|
50
|
+
}, p = () => /* @__PURE__ */ c("div", {
|
|
51
|
+
style: u.LOADING,
|
|
52
|
+
children: l.LOADING
|
|
53
|
+
});
|
|
54
|
+
function m(e) {
|
|
55
|
+
let { "data-widget-id": t, "data-widget-type": r, ...i } = e;
|
|
56
|
+
return /* @__PURE__ */ c("div", {
|
|
57
|
+
...i,
|
|
58
|
+
children: /* @__PURE__ */ c(f, {
|
|
59
|
+
widgetId: t || l.UNKNOWN,
|
|
60
|
+
widgetType: r || l.UNKNOWN,
|
|
61
|
+
children: /* @__PURE__ */ c(n, {
|
|
62
|
+
fallback: /* @__PURE__ */ c(p, {}),
|
|
63
|
+
children: e.children
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
var h = e.createContext({
|
|
69
|
+
items: [],
|
|
70
|
+
ItemWrapper: (e) => /* @__PURE__ */ c("div", { ...e })
|
|
71
|
+
});
|
|
72
|
+
function g(e, t, n, r) {
|
|
73
|
+
let i = Object.prototype.hasOwnProperty.call(t, e.type) ? t[e.type] : void 0;
|
|
74
|
+
if (!i) return console.warn(l.UNKNOWN_WIDGET(e.type, e.id)), null;
|
|
75
|
+
let a = e.children ?? [];
|
|
76
|
+
return /* @__PURE__ */ c(h.Provider, {
|
|
77
|
+
value: {
|
|
78
|
+
items: a,
|
|
79
|
+
ItemWrapper: n
|
|
80
|
+
},
|
|
81
|
+
children: /* @__PURE__ */ c(n, {
|
|
82
|
+
"data-widget-id": e.id,
|
|
83
|
+
"data-widget-type": e.type,
|
|
84
|
+
children: /* @__PURE__ */ c(i, {
|
|
85
|
+
...e.props,
|
|
86
|
+
Output: r
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}, e.id);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/widget.tsx
|
|
93
|
+
function _(e) {
|
|
94
|
+
let { components: t, chrome: n, context: l } = e, u = l || r(t), f = u.Provider, p = () => a(u), _ = i(function e() {
|
|
95
|
+
let { items: t, ItemWrapper: n } = a(h), r = p();
|
|
96
|
+
return !t || t.length === 0 ? null : /* @__PURE__ */ c(s, { children: t.map((t) => g(t, r, n, e)) });
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
Widgets: i(function({ items: e, components: r, chrome: i }) {
|
|
100
|
+
let a = i?.wrapper || n?.wrapper || d, s = i?.item || n?.item || m, l = o(() => ({
|
|
101
|
+
...t,
|
|
102
|
+
...r
|
|
103
|
+
}), [r]);
|
|
104
|
+
return /* @__PURE__ */ c(f, {
|
|
105
|
+
value: l,
|
|
106
|
+
children: /* @__PURE__ */ c(a, { children: e.map((e) => g(e, l, s, _)) })
|
|
107
|
+
});
|
|
108
|
+
}),
|
|
109
|
+
WidgetsProvider: f,
|
|
110
|
+
useWidgets: p,
|
|
111
|
+
Output: _,
|
|
112
|
+
defineItems: (e) => e
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { u as DEFAULT_STYLES, m as DefaultItem, d as DefaultWrapper, l as ERROR_MESSAGES, h as NestedWidgetsContext, _ as createWidgets, g as renderWidget };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ComponentProps, ComponentType, Context, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The prop every widget component receives so it can render its own children.
|
|
4
|
+
* Injected by the renderer; it is never part of the item data.
|
|
5
|
+
*/
|
|
6
|
+
export interface WidgetOutputProps {
|
|
7
|
+
Output: ComponentType;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Any widget component, for use in a generic *constraint*.
|
|
11
|
+
*
|
|
12
|
+
* `any` is load-bearing here and cannot be tightened. `ComponentType<unknown>`
|
|
13
|
+
* would reject a component with concrete props, because component props are
|
|
14
|
+
* contravariant -- a `ComponentType<{title: string}>` is not assignable to a
|
|
15
|
+
* `ComponentType<unknown>`. TypeScript offers no "some component, props
|
|
16
|
+
* unknown" type for this position.
|
|
17
|
+
*
|
|
18
|
+
* This does not weaken inference: `ComponentProps<C[K]>` below resolves against
|
|
19
|
+
* the concrete component that was actually passed, not against this constraint.
|
|
20
|
+
*/
|
|
21
|
+
export type AnyWidgetComponent = ComponentType<any>;
|
|
22
|
+
/**
|
|
23
|
+
* A map of widget type name -> component.
|
|
24
|
+
*
|
|
25
|
+
* This is the type that drives inference for a whole widget set: pass a literal
|
|
26
|
+
* object to {@link createWidgets} and every item's `type` and `props` are
|
|
27
|
+
* checked against it.
|
|
28
|
+
*/
|
|
29
|
+
export type WidgetComponentMap = Record<string, AnyWidgetComponent>;
|
|
30
|
+
/**
|
|
31
|
+
* The props a widget component accepts as *data*, i.e. everything except the
|
|
32
|
+
* injected {@link WidgetOutputProps.Output}.
|
|
33
|
+
*/
|
|
34
|
+
export type WidgetDataProps<C extends AnyWidgetComponent> = Omit<ComponentProps<C>, keyof WidgetOutputProps>;
|
|
35
|
+
/**
|
|
36
|
+
* A single item in a widget set, discriminated on `type`.
|
|
37
|
+
*
|
|
38
|
+
* Distributing over the keys of the component map is what makes this checked:
|
|
39
|
+
* `type: 'news'` forces `props` to the props of the `news` component, and an
|
|
40
|
+
* unknown `type` is a compile error rather than a runtime `console.warn`.
|
|
41
|
+
*/
|
|
42
|
+
export type WidgetItem<C extends WidgetComponentMap> = {
|
|
43
|
+
[K in keyof C & string]: {
|
|
44
|
+
/** Stable identity for this item; used as the React key. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Which component to render. Must be a key of the component map. */
|
|
47
|
+
type: K;
|
|
48
|
+
/** Props for that component, minus the injected `Output`. */
|
|
49
|
+
props: WidgetDataProps<C[K]>;
|
|
50
|
+
/** Nested items, rendered by the item's injected `Output`. */
|
|
51
|
+
children?: WidgetItem<C>[];
|
|
52
|
+
};
|
|
53
|
+
}[keyof C & string];
|
|
54
|
+
/**
|
|
55
|
+
* Loose item shape, for callers that build item data before a component map
|
|
56
|
+
* exists (a CMS payload, a fixture, a network response).
|
|
57
|
+
*
|
|
58
|
+
* Prefer {@link WidgetItem}, which is checked against the component map.
|
|
59
|
+
*/
|
|
60
|
+
export interface WidgetProps<Type extends string = string, Props = object> {
|
|
61
|
+
id: string;
|
|
62
|
+
type: Type;
|
|
63
|
+
props: Props;
|
|
64
|
+
children?: WidgetProps[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Type-erased view of an item, used internally by the renderer.
|
|
68
|
+
*
|
|
69
|
+
* The checked {@link WidgetItem} union is widened to this exactly once, at the
|
|
70
|
+
* boundary between the public props and the render loop. Spreading the
|
|
71
|
+
* discriminated union directly onto a component makes TypeScript give up with
|
|
72
|
+
* "union type that is too complex to represent" (TS2590), and the renderer
|
|
73
|
+
* gains nothing from the discrimination -- it looks the type up at runtime.
|
|
74
|
+
*/
|
|
75
|
+
export interface RenderableWidgetItem {
|
|
76
|
+
id: string;
|
|
77
|
+
type: string;
|
|
78
|
+
props: Record<string, unknown>;
|
|
79
|
+
children?: RenderableWidgetItem[];
|
|
80
|
+
}
|
|
81
|
+
/** Chrome wrapped around the whole widget set. */
|
|
82
|
+
export type WidgetsWrapperComponent = ComponentType<{
|
|
83
|
+
children?: ReactNode;
|
|
84
|
+
}>;
|
|
85
|
+
/** Chrome wrapped around each individual widget. */
|
|
86
|
+
export type WidgetItemComponent = ComponentType<{
|
|
87
|
+
children?: ReactNode;
|
|
88
|
+
'data-widget-id': string;
|
|
89
|
+
'data-widget-type': string;
|
|
90
|
+
}>;
|
|
91
|
+
export interface WidgetsChrome {
|
|
92
|
+
wrapper?: WidgetsWrapperComponent;
|
|
93
|
+
item?: WidgetItemComponent;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Configuration for {@link createWidgets}.
|
|
97
|
+
*/
|
|
98
|
+
export interface WidgetsConfig<C extends WidgetComponentMap> {
|
|
99
|
+
/** The component map. Its shape drives inference for the whole set. */
|
|
100
|
+
components: C;
|
|
101
|
+
chrome?: WidgetsChrome;
|
|
102
|
+
/** Supply your own context to share a component map across widget sets. */
|
|
103
|
+
context?: Context<C>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Props of the `Widgets` component returned by {@link createWidgets}.
|
|
107
|
+
*/
|
|
108
|
+
export interface WidgetsProps<C extends WidgetComponentMap> {
|
|
109
|
+
items: WidgetItem<C>[];
|
|
110
|
+
/** Per-instance component overrides, merged over the factory's map. */
|
|
111
|
+
components?: Partial<C>;
|
|
112
|
+
/** Per-instance chrome overrides. */
|
|
113
|
+
chrome?: WidgetsChrome;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE/E;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;AAEpE;;;GAGG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,kBAAkB,IAAI,IAAI,CAC9D,cAAc,CAAC,CAAC,CAAC,EACjB,MAAM,iBAAiB,CACxB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,kBAAkB,IAAI;KACpD,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,GAAG;QACvB,4DAA4D;QAC5D,EAAE,EAAE,MAAM,CAAC;QACX,qEAAqE;QACrE,IAAI,EAAE,CAAC,CAAC;QACR,6DAA6D;QAC7D,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,8DAA8D;QAC9D,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5B;CACF,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAEpB;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,MAAM;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACnC;AAED,kDAAkD;AAClD,MAAM,MAAM,uBAAuB,GAAG,aAAa,CAAC;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AAE9E,oDAAoD;AACpD,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC;IAC9C,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC,CAAC;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,kBAAkB;IACzD,uEAAuE;IACvE,UAAU,EAAE,CAAC,CAAC;IACd,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,kBAAkB;IACxD,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IACvB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACxB,qCAAqC;IACrC,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { AnyWidgetComponent, RenderableWidgetItem, WidgetItemComponent } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Context carrying the current widget's children down to the injected `Output`
|
|
5
|
+
* component, together with the chrome resolved for this render.
|
|
6
|
+
*
|
|
7
|
+
* Passing children through context (rather than closing over them in a
|
|
8
|
+
* freshly-created component) is what keeps `Output` a single stable component
|
|
9
|
+
* type, and what lets nesting recurse to arbitrary depth.
|
|
10
|
+
*/
|
|
11
|
+
export interface NestedWidgets {
|
|
12
|
+
items: RenderableWidgetItem[];
|
|
13
|
+
ItemWrapper: WidgetItemComponent;
|
|
14
|
+
}
|
|
15
|
+
export declare const NestedWidgetsContext: React.Context<NestedWidgets>;
|
|
16
|
+
export declare function renderWidget(item: RenderableWidgetItem, components: Record<string, AnyWidgetComponent>, ItemWrapper: WidgetItemComponent, Output: React.ComponentType): React.JSX.Element | null;
|
|
17
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAMD,eAAO,MAAM,oBAAoB,8BAG/B,CAAC;AAEH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,oBAAoB,EAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAC9C,WAAW,EAAE,mBAAmB,EAChC,MAAM,EAAE,KAAK,CAAC,aAAa,4BA+B5B"}
|
package/dist/widget.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WidgetComponentMap, WidgetItem, WidgetsConfig, WidgetsProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a widget set from a component map.
|
|
4
|
+
*
|
|
5
|
+
* The map drives inference: each item's `type` must be a key of it, and that
|
|
6
|
+
* item's `props` must match the corresponding component's props.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const { Widgets } = createWidgets({
|
|
11
|
+
* components: { news: NewsTeaser, profile: UserSidebar },
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* <Widgets items={[
|
|
15
|
+
* { id: '1', type: 'news', props: { title: 'Hello' } },
|
|
16
|
+
* { id: '2', type: 'nope', props: {} }, // ← compile error: unknown type
|
|
17
|
+
* ]} />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function createWidgets<const C extends WidgetComponentMap>(config: WidgetsConfig<C>): {
|
|
21
|
+
Widgets: import('react').MemoExoticComponent<({ items, components: instanceComponents, chrome, }: WidgetsProps<C>) => import("react").JSX.Element>;
|
|
22
|
+
WidgetsProvider: import('react').Provider<C>;
|
|
23
|
+
useWidgets: () => C;
|
|
24
|
+
Output: import('react').MemoExoticComponent<() => import("react").JSX.Element | null>;
|
|
25
|
+
defineItems: (items: WidgetItem<C>[]) => WidgetItem<C>[];
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=widget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.d.ts","sourceRoot":"","sources":["../src/widget.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,YAAY,EACb,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,KAAK,CAAC,CAAC,SAAS,kBAAkB,EAC9D,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;sGA2CrB,YAAY,CAAC,CAAC,CAAC;;;;yBAsCU,UAAU,CAAC,CAAC,CAAC,EAAE,KAAG,UAAU,CAAC,CAAC,CAAC,EAAE;EAG9D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { default as React, HTMLProps } from 'react';
|
|
2
|
+
export declare function DefaultWrapper(props: HTMLProps<HTMLDivElement>): React.JSX.Element;
|
|
3
|
+
export declare function DefaultItem(props: HTMLProps<HTMLDivElement> & {
|
|
4
|
+
'data-widget-id'?: string;
|
|
5
|
+
'data-widget-type'?: string;
|
|
6
|
+
}): React.JSX.Element;
|
|
7
|
+
//# sourceMappingURL=widgets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widgets.d.ts","sourceRoot":"","sources":["../src/widgets.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAkC,MAAM,OAAO,CAAC;AAGzE,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,cAAc,CAAC,qBAE9D;AA8CD,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,CAAC,cAAc,CAAC,GAAG;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,qBAoBF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@evanion/react-widget",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Render dynamic, type-safe React widget regions from structured data. Built for CMS-driven layouts, dashboards and configurable sidebars.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"widget",
|
|
8
|
+
"cms",
|
|
9
|
+
"dynamic",
|
|
10
|
+
"layout",
|
|
11
|
+
"components",
|
|
12
|
+
"registry",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/Evanion/libraries/tree/main/libs/widget#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/Evanion/libraries/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Evanion/libraries.git",
|
|
22
|
+
"directory": "libs/widget"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Mikael Pettersson",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": "./package.json",
|
|
36
|
+
".": {
|
|
37
|
+
"@evanion/source": "./src/index.ts",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"!dist/**/*.tsbuildinfo",
|
|
46
|
+
"src",
|
|
47
|
+
"!src/**/*.test.*",
|
|
48
|
+
"!src/**/*.spec.*",
|
|
49
|
+
"!src/**/*.test-d.*",
|
|
50
|
+
"!src/test-setup.ts",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"CHANGELOG.md"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"provenance": true
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const ERROR_MESSAGES = {
|
|
2
|
+
UNKNOWN_WIDGET: (type: string, id: string) =>
|
|
3
|
+
`Unknown widget type "${type}" for widget ID "${id}". Skipping render.`,
|
|
4
|
+
WIDGET_ERROR: (type: string, id: string) =>
|
|
5
|
+
`Widget Error: ${type} (ID: ${id})`,
|
|
6
|
+
WIDGET_FAILED: (type: string) => `Widget failed to render: ${type}`,
|
|
7
|
+
LOADING: 'Loading widget...',
|
|
8
|
+
UNKNOWN: 'unknown',
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_STYLES = {
|
|
12
|
+
ERROR: {
|
|
13
|
+
padding: '8px',
|
|
14
|
+
border: '1px solid #ff6b6b',
|
|
15
|
+
borderRadius: '4px',
|
|
16
|
+
backgroundColor: '#ffe0e0',
|
|
17
|
+
color: '#d63031',
|
|
18
|
+
},
|
|
19
|
+
LOADING: {
|
|
20
|
+
padding: '8px',
|
|
21
|
+
border: '1px solid #ddd',
|
|
22
|
+
borderRadius: '4px',
|
|
23
|
+
backgroundColor: '#f8f9fa',
|
|
24
|
+
color: '#666',
|
|
25
|
+
},
|
|
26
|
+
} as const;
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ComponentProps, ComponentType, Context, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The prop every widget component receives so it can render its own children.
|
|
5
|
+
* Injected by the renderer; it is never part of the item data.
|
|
6
|
+
*/
|
|
7
|
+
export interface WidgetOutputProps {
|
|
8
|
+
Output: ComponentType;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Any widget component, for use in a generic *constraint*.
|
|
13
|
+
*
|
|
14
|
+
* `any` is load-bearing here and cannot be tightened. `ComponentType<unknown>`
|
|
15
|
+
* would reject a component with concrete props, because component props are
|
|
16
|
+
* contravariant -- a `ComponentType<{title: string}>` is not assignable to a
|
|
17
|
+
* `ComponentType<unknown>`. TypeScript offers no "some component, props
|
|
18
|
+
* unknown" type for this position.
|
|
19
|
+
*
|
|
20
|
+
* This does not weaken inference: `ComponentProps<C[K]>` below resolves against
|
|
21
|
+
* the concrete component that was actually passed, not against this constraint.
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
export type AnyWidgetComponent = ComponentType<any>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A map of widget type name -> component.
|
|
28
|
+
*
|
|
29
|
+
* This is the type that drives inference for a whole widget set: pass a literal
|
|
30
|
+
* object to {@link createWidgets} and every item's `type` and `props` are
|
|
31
|
+
* checked against it.
|
|
32
|
+
*/
|
|
33
|
+
export type WidgetComponentMap = Record<string, AnyWidgetComponent>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The props a widget component accepts as *data*, i.e. everything except the
|
|
37
|
+
* injected {@link WidgetOutputProps.Output}.
|
|
38
|
+
*/
|
|
39
|
+
export type WidgetDataProps<C extends AnyWidgetComponent> = Omit<
|
|
40
|
+
ComponentProps<C>,
|
|
41
|
+
keyof WidgetOutputProps
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A single item in a widget set, discriminated on `type`.
|
|
46
|
+
*
|
|
47
|
+
* Distributing over the keys of the component map is what makes this checked:
|
|
48
|
+
* `type: 'news'` forces `props` to the props of the `news` component, and an
|
|
49
|
+
* unknown `type` is a compile error rather than a runtime `console.warn`.
|
|
50
|
+
*/
|
|
51
|
+
export type WidgetItem<C extends WidgetComponentMap> = {
|
|
52
|
+
[K in keyof C & string]: {
|
|
53
|
+
/** Stable identity for this item; used as the React key. */
|
|
54
|
+
id: string;
|
|
55
|
+
/** Which component to render. Must be a key of the component map. */
|
|
56
|
+
type: K;
|
|
57
|
+
/** Props for that component, minus the injected `Output`. */
|
|
58
|
+
props: WidgetDataProps<C[K]>;
|
|
59
|
+
/** Nested items, rendered by the item's injected `Output`. */
|
|
60
|
+
children?: WidgetItem<C>[];
|
|
61
|
+
};
|
|
62
|
+
}[keyof C & string];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Loose item shape, for callers that build item data before a component map
|
|
66
|
+
* exists (a CMS payload, a fixture, a network response).
|
|
67
|
+
*
|
|
68
|
+
* Prefer {@link WidgetItem}, which is checked against the component map.
|
|
69
|
+
*/
|
|
70
|
+
export interface WidgetProps<Type extends string = string, Props = object> {
|
|
71
|
+
id: string;
|
|
72
|
+
type: Type;
|
|
73
|
+
props: Props;
|
|
74
|
+
children?: WidgetProps[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Type-erased view of an item, used internally by the renderer.
|
|
79
|
+
*
|
|
80
|
+
* The checked {@link WidgetItem} union is widened to this exactly once, at the
|
|
81
|
+
* boundary between the public props and the render loop. Spreading the
|
|
82
|
+
* discriminated union directly onto a component makes TypeScript give up with
|
|
83
|
+
* "union type that is too complex to represent" (TS2590), and the renderer
|
|
84
|
+
* gains nothing from the discrimination -- it looks the type up at runtime.
|
|
85
|
+
*/
|
|
86
|
+
export interface RenderableWidgetItem {
|
|
87
|
+
id: string;
|
|
88
|
+
type: string;
|
|
89
|
+
props: Record<string, unknown>;
|
|
90
|
+
children?: RenderableWidgetItem[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Chrome wrapped around the whole widget set. */
|
|
94
|
+
export type WidgetsWrapperComponent = ComponentType<{ children?: ReactNode }>;
|
|
95
|
+
|
|
96
|
+
/** Chrome wrapped around each individual widget. */
|
|
97
|
+
export type WidgetItemComponent = ComponentType<{
|
|
98
|
+
children?: ReactNode;
|
|
99
|
+
'data-widget-id': string;
|
|
100
|
+
'data-widget-type': string;
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
export interface WidgetsChrome {
|
|
104
|
+
wrapper?: WidgetsWrapperComponent;
|
|
105
|
+
item?: WidgetItemComponent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Configuration for {@link createWidgets}.
|
|
110
|
+
*/
|
|
111
|
+
export interface WidgetsConfig<C extends WidgetComponentMap> {
|
|
112
|
+
/** The component map. Its shape drives inference for the whole set. */
|
|
113
|
+
components: C;
|
|
114
|
+
chrome?: WidgetsChrome;
|
|
115
|
+
/** Supply your own context to share a component map across widget sets. */
|
|
116
|
+
context?: Context<C>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Props of the `Widgets` component returned by {@link createWidgets}.
|
|
121
|
+
*/
|
|
122
|
+
export interface WidgetsProps<C extends WidgetComponentMap> {
|
|
123
|
+
items: WidgetItem<C>[];
|
|
124
|
+
/** Per-instance component overrides, merged over the factory's map. */
|
|
125
|
+
components?: Partial<C>;
|
|
126
|
+
/** Per-instance chrome overrides. */
|
|
127
|
+
chrome?: WidgetsChrome;
|
|
128
|
+
}
|
package/src/utils.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ERROR_MESSAGES } from './constants.js';
|
|
3
|
+
import type {
|
|
4
|
+
AnyWidgetComponent,
|
|
5
|
+
RenderableWidgetItem,
|
|
6
|
+
WidgetItemComponent,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context carrying the current widget's children down to the injected `Output`
|
|
11
|
+
* component, together with the chrome resolved for this render.
|
|
12
|
+
*
|
|
13
|
+
* Passing children through context (rather than closing over them in a
|
|
14
|
+
* freshly-created component) is what keeps `Output` a single stable component
|
|
15
|
+
* type, and what lets nesting recurse to arbitrary depth.
|
|
16
|
+
*/
|
|
17
|
+
export interface NestedWidgets {
|
|
18
|
+
items: RenderableWidgetItem[];
|
|
19
|
+
ItemWrapper: WidgetItemComponent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DefaultNestedItemWrapper: WidgetItemComponent = (props) => (
|
|
23
|
+
<div {...props} />
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const NestedWidgetsContext = React.createContext<NestedWidgets>({
|
|
27
|
+
items: [],
|
|
28
|
+
ItemWrapper: DefaultNestedItemWrapper,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function renderWidget(
|
|
32
|
+
item: RenderableWidgetItem,
|
|
33
|
+
components: Record<string, AnyWidgetComponent>,
|
|
34
|
+
ItemWrapper: WidgetItemComponent,
|
|
35
|
+
Output: React.ComponentType,
|
|
36
|
+
) {
|
|
37
|
+
// `in` walks the prototype chain, so a CMS-supplied type of "constructor",
|
|
38
|
+
// "toString" or "__proto__" would pass this guard and hand React something
|
|
39
|
+
// off Object.prototype. Items are explicitly untrusted input.
|
|
40
|
+
//
|
|
41
|
+
// The truthiness check is not redundant: an own key can still hold undefined,
|
|
42
|
+
// and noUncheckedIndexedAccess makes that possibility explicit.
|
|
43
|
+
const Component = Object.prototype.hasOwnProperty.call(components, item.type)
|
|
44
|
+
? components[item.type]
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
47
|
+
if (!Component) {
|
|
48
|
+
console.warn(ERROR_MESSAGES.UNKNOWN_WIDGET(item.type, item.id));
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const children = item.children ?? [];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
// Each widget provides its own children, so a nested `Output` renders that
|
|
56
|
+
// widget's children rather than its parent's -- at any depth.
|
|
57
|
+
<NestedWidgetsContext.Provider
|
|
58
|
+
key={item.id}
|
|
59
|
+
value={{ items: children, ItemWrapper }}
|
|
60
|
+
>
|
|
61
|
+
<ItemWrapper data-widget-id={item.id} data-widget-type={item.type}>
|
|
62
|
+
<Component {...item.props} Output={Output} />
|
|
63
|
+
</ItemWrapper>
|
|
64
|
+
</NestedWidgetsContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
}
|
package/src/widget.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, memo } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
RenderableWidgetItem,
|
|
4
|
+
WidgetComponentMap,
|
|
5
|
+
WidgetItem,
|
|
6
|
+
WidgetsConfig,
|
|
7
|
+
WidgetsProps,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
import { DefaultItem, DefaultWrapper } from './widgets.js';
|
|
10
|
+
import { renderWidget, NestedWidgetsContext } from './utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds a widget set from a component map.
|
|
14
|
+
*
|
|
15
|
+
* The map drives inference: each item's `type` must be a key of it, and that
|
|
16
|
+
* item's `props` must match the corresponding component's props.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { Widgets } = createWidgets({
|
|
21
|
+
* components: { news: NewsTeaser, profile: UserSidebar },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* <Widgets items={[
|
|
25
|
+
* { id: '1', type: 'news', props: { title: 'Hello' } },
|
|
26
|
+
* { id: '2', type: 'nope', props: {} }, // ← compile error: unknown type
|
|
27
|
+
* ]} />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function createWidgets<const C extends WidgetComponentMap>(
|
|
31
|
+
config: WidgetsConfig<C>,
|
|
32
|
+
) {
|
|
33
|
+
const {
|
|
34
|
+
components: defaultComponents,
|
|
35
|
+
chrome: defaultChrome,
|
|
36
|
+
context,
|
|
37
|
+
} = config;
|
|
38
|
+
|
|
39
|
+
const WidgetsContext = context || createContext<C>(defaultComponents);
|
|
40
|
+
|
|
41
|
+
const WidgetsProvider = WidgetsContext.Provider;
|
|
42
|
+
const useWidgets = () => useContext(WidgetsContext);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Renders the children of whichever widget is currently rendering.
|
|
46
|
+
*
|
|
47
|
+
* Defined once per `createWidgets` call, so its component type is stable for
|
|
48
|
+
* the lifetime of the factory. Previously a new component was built on every
|
|
49
|
+
* render, and because React compares element types by identity that
|
|
50
|
+
* unmounted and remounted every nested subtree on each parent render --
|
|
51
|
+
* discarding child state, effects and focus.
|
|
52
|
+
*/
|
|
53
|
+
const Output = memo(function Output() {
|
|
54
|
+
const { items, ItemWrapper } = useContext(NestedWidgetsContext);
|
|
55
|
+
const components = useWidgets();
|
|
56
|
+
|
|
57
|
+
if (!items || items.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{items.map((item) =>
|
|
64
|
+
renderWidget(item, components, ItemWrapper, Output),
|
|
65
|
+
)}
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const Widgets = memo(function Widgets({
|
|
71
|
+
items,
|
|
72
|
+
components: instanceComponents,
|
|
73
|
+
chrome,
|
|
74
|
+
}: WidgetsProps<C>) {
|
|
75
|
+
const Wrapper = chrome?.wrapper || defaultChrome?.wrapper || DefaultWrapper;
|
|
76
|
+
const ItemWrapper = chrome?.item || defaultChrome?.item || DefaultItem;
|
|
77
|
+
const components = useMemo(
|
|
78
|
+
() => ({ ...defaultComponents, ...instanceComponents }),
|
|
79
|
+
[instanceComponents],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<WidgetsProvider value={components}>
|
|
84
|
+
<Wrapper>
|
|
85
|
+
{/* The single, documented widening from the checked WidgetItem<C>
|
|
86
|
+
union to the renderer's erased view. See RenderableWidgetItem. */}
|
|
87
|
+
{(items as unknown as RenderableWidgetItem[]).map((item) =>
|
|
88
|
+
renderWidget(item, components, ItemWrapper, Output),
|
|
89
|
+
)}
|
|
90
|
+
</Wrapper>
|
|
91
|
+
</WidgetsProvider>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Identity function that supplies the contextual type for an item array.
|
|
97
|
+
*
|
|
98
|
+
* A bare `const items = [{ type: 'news', ... }]` infers `type: string`, which
|
|
99
|
+
* will not narrow to the component map's keys, so the check is lost. Passing
|
|
100
|
+
* the array through here gives TypeScript the contextual type it needs:
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* const items = defineItems([
|
|
104
|
+
* { id: '1', type: 'news', props: { title: 'Hello' } },
|
|
105
|
+
* { id: '2', type: 'nope', props: {} }, // ← compile error
|
|
106
|
+
* ]);
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* Not needed when the array is written inline in JSX -- that is already
|
|
110
|
+
* contextually typed. `satisfies WidgetItem<typeof components>[]` works too.
|
|
111
|
+
*/
|
|
112
|
+
const defineItems = (items: WidgetItem<C>[]): WidgetItem<C>[] => items;
|
|
113
|
+
|
|
114
|
+
return { Widgets, WidgetsProvider, useWidgets, Output, defineItems };
|
|
115
|
+
}
|
package/src/widgets.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { HTMLProps, Suspense, Component, ReactNode } from 'react';
|
|
2
|
+
import { ERROR_MESSAGES, DEFAULT_STYLES } from './constants.js';
|
|
3
|
+
|
|
4
|
+
export function DefaultWrapper(props: HTMLProps<HTMLDivElement>) {
|
|
5
|
+
return <section {...props} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Error boundary for individual widgets
|
|
9
|
+
class WidgetErrorBoundary extends Component<
|
|
10
|
+
{ children: ReactNode; widgetId: string; widgetType: string },
|
|
11
|
+
{ hasError: boolean; error?: Error }
|
|
12
|
+
> {
|
|
13
|
+
constructor(props: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
widgetId: string;
|
|
16
|
+
widgetType: string;
|
|
17
|
+
}) {
|
|
18
|
+
super(props);
|
|
19
|
+
this.state = { hasError: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static getDerivedStateFromError(error: Error) {
|
|
23
|
+
return { hasError: true, error };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
27
|
+
console.error(
|
|
28
|
+
ERROR_MESSAGES.WIDGET_ERROR(this.props.widgetType, this.props.widgetId),
|
|
29
|
+
error,
|
|
30
|
+
errorInfo,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override render() {
|
|
35
|
+
if (this.state.hasError) {
|
|
36
|
+
return (
|
|
37
|
+
<div style={DEFAULT_STYLES.ERROR}>
|
|
38
|
+
{ERROR_MESSAGES.WIDGET_FAILED(this.props.widgetType)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this.props.children;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default loading fallback
|
|
48
|
+
const DefaultLoadingFallback = () => (
|
|
49
|
+
<div style={DEFAULT_STYLES.LOADING}>{ERROR_MESSAGES.LOADING}</div>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export function DefaultItem(
|
|
53
|
+
props: HTMLProps<HTMLDivElement> & {
|
|
54
|
+
'data-widget-id'?: string;
|
|
55
|
+
'data-widget-type'?: string;
|
|
56
|
+
},
|
|
57
|
+
) {
|
|
58
|
+
const {
|
|
59
|
+
'data-widget-id': widgetId,
|
|
60
|
+
'data-widget-type': widgetType,
|
|
61
|
+
...restProps
|
|
62
|
+
} = props;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div {...restProps}>
|
|
66
|
+
<WidgetErrorBoundary
|
|
67
|
+
widgetId={widgetId || ERROR_MESSAGES.UNKNOWN}
|
|
68
|
+
widgetType={widgetType || ERROR_MESSAGES.UNKNOWN}
|
|
69
|
+
>
|
|
70
|
+
<Suspense fallback={<DefaultLoadingFallback />}>
|
|
71
|
+
{props.children}
|
|
72
|
+
</Suspense>
|
|
73
|
+
</WidgetErrorBoundary>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|