react-solidlike 2.2.4 → 2.4.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/README.en.md +129 -0
- package/README.md +129 -0
- package/dist/ClientOnly.d.ts +43 -0
- package/dist/Once.d.ts +43 -0
- package/dist/Repeat.d.ts +4 -9
- package/dist/Show.d.ts +9 -1
- package/dist/Split.d.ts +49 -0
- package/dist/Visible.d.ts +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +109 -10
- package/package.json +3 -1
package/README.en.md
CHANGED
|
@@ -35,6 +35,11 @@ import { Show } from "react-solidlike";
|
|
|
35
35
|
<Show when={user}>
|
|
36
36
|
{(user) => <UserProfile name={user.name} />}
|
|
37
37
|
</Show>
|
|
38
|
+
|
|
39
|
+
// With onFallback callback (for redirects and other side effects)
|
|
40
|
+
<Show when={isAuthenticated} fallback={<Loading />} onFallback={() => navigate('/login')}>
|
|
41
|
+
<Dashboard />
|
|
42
|
+
</Show>
|
|
38
43
|
```
|
|
39
44
|
|
|
40
45
|
### `<For>` - List Rendering
|
|
@@ -166,6 +171,48 @@ import { Repeat } from "react-solidlike";
|
|
|
166
171
|
</Repeat>
|
|
167
172
|
```
|
|
168
173
|
|
|
174
|
+
### `<Split>` - String Split Rendering
|
|
175
|
+
|
|
176
|
+
Split a string by separator and render each part.
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { Split } from "react-solidlike";
|
|
180
|
+
|
|
181
|
+
// Basic usage - splits without keeping separator
|
|
182
|
+
<Split string="a,b,c" separator=",">
|
|
183
|
+
{(part) => <span>{part}</span>}
|
|
184
|
+
</Split>
|
|
185
|
+
// Renders: ["a", "b", "c"]
|
|
186
|
+
|
|
187
|
+
// Keep separator in result
|
|
188
|
+
<Split string="9+5=(9+1)+4" separator="=" keepSeparator>
|
|
189
|
+
{(part) => <span>{part}</span>}
|
|
190
|
+
</Split>
|
|
191
|
+
// Renders: ["9+5", "=", "(9+1)+4"]
|
|
192
|
+
|
|
193
|
+
// Using RegExp separator
|
|
194
|
+
<Split string="a1b2c3" separator={/\d/} keepSeparator>
|
|
195
|
+
{(part) => <span>{part}</span>}
|
|
196
|
+
</Split>
|
|
197
|
+
// Renders: ["a", "1", "b", "2", "c", "3"]
|
|
198
|
+
|
|
199
|
+
// With wrapper element
|
|
200
|
+
<Split string="hello world" separator=" " wrapper={<div className="words" />}>
|
|
201
|
+
{(word) => <span>{word}</span>}
|
|
202
|
+
</Split>
|
|
203
|
+
|
|
204
|
+
// With fallback for empty string
|
|
205
|
+
<Split string={text} separator="," fallback={<EmptyState />}>
|
|
206
|
+
{(part) => <Tag>{part}</Tag>}
|
|
207
|
+
</Split>
|
|
208
|
+
|
|
209
|
+
// Reverse rendering
|
|
210
|
+
<Split string="a,b,c" separator="," reverse>
|
|
211
|
+
{(part) => <span>{part}</span>}
|
|
212
|
+
</Split>
|
|
213
|
+
// Render order: ["c", "b", "a"]
|
|
214
|
+
```
|
|
215
|
+
|
|
169
216
|
### `<Dynamic>` - Dynamic Component
|
|
170
217
|
|
|
171
218
|
Dynamically select component type based on conditions.
|
|
@@ -268,6 +315,88 @@ function UserList() {
|
|
|
268
315
|
| `children` | `ReactNode \| (data: T) => ReactNode` | Success content |
|
|
269
316
|
| `isEmptyFn` | `(data: T) => boolean` | Custom empty check |
|
|
270
317
|
|
|
318
|
+
### `<Once>` - Single Render
|
|
319
|
+
|
|
320
|
+
Renders children only once and ignores subsequent updates. Useful for expensive computations or content that shouldn't re-render.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { Once } from "react-solidlike";
|
|
324
|
+
|
|
325
|
+
// Render expensive component once
|
|
326
|
+
<Once>
|
|
327
|
+
<ExpensiveChart data={data} />
|
|
328
|
+
</Once>
|
|
329
|
+
|
|
330
|
+
// Prevent re-renders from parent updates
|
|
331
|
+
function Parent() {
|
|
332
|
+
const [count, setCount] = useState(0);
|
|
333
|
+
return (
|
|
334
|
+
<div>
|
|
335
|
+
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
|
|
336
|
+
<Once>
|
|
337
|
+
<Child initialCount={count} />
|
|
338
|
+
</Once>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### `<ClientOnly>` - Client-side Only Rendering
|
|
345
|
+
|
|
346
|
+
Renders children only on the client side (after hydration). Useful for components that rely on browser APIs or need to avoid SSR hydration mismatches.
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { ClientOnly } from "react-solidlike";
|
|
350
|
+
|
|
351
|
+
// Basic usage
|
|
352
|
+
<ClientOnly>
|
|
353
|
+
<BrowserOnlyComponent />
|
|
354
|
+
</ClientOnly>
|
|
355
|
+
|
|
356
|
+
// With SSR fallback
|
|
357
|
+
<ClientOnly fallback={<Skeleton />}>
|
|
358
|
+
<DynamicChart />
|
|
359
|
+
</ClientOnly>
|
|
360
|
+
|
|
361
|
+
// Using render function for lazy evaluation (avoid accessing window)
|
|
362
|
+
<ClientOnly fallback={<Loading />}>
|
|
363
|
+
{() => <ComponentUsingWindow width={window.innerWidth} />}
|
|
364
|
+
</ClientOnly>
|
|
365
|
+
|
|
366
|
+
// Avoid hydration mismatch
|
|
367
|
+
<ClientOnly fallback={<span>--:--</span>}>
|
|
368
|
+
<CurrentTime />
|
|
369
|
+
</ClientOnly>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `<Visible>` - Visibility-based Rendering (Web only)
|
|
373
|
+
|
|
374
|
+
Renders children when entering viewport using IntersectionObserver. In React Native or unsupported environments, children are rendered directly (graceful degradation).
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { Visible } from "react-solidlike";
|
|
378
|
+
|
|
379
|
+
// Basic usage - render when entering viewport
|
|
380
|
+
<Visible>
|
|
381
|
+
<HeavyComponent />
|
|
382
|
+
</Visible>
|
|
383
|
+
|
|
384
|
+
// With placeholder
|
|
385
|
+
<Visible fallback={<Skeleton />}>
|
|
386
|
+
<Image src={url} />
|
|
387
|
+
</Visible>
|
|
388
|
+
|
|
389
|
+
// Preload before entering viewport (rootMargin)
|
|
390
|
+
<Visible rootMargin="200px" fallback={<Placeholder />}>
|
|
391
|
+
<LazyImage />
|
|
392
|
+
</Visible>
|
|
393
|
+
|
|
394
|
+
// Toggle visibility (once=false unmounts when leaving viewport)
|
|
395
|
+
<Visible once={false} onVisibilityChange={(v) => console.log(v)}>
|
|
396
|
+
<VideoPlayer />
|
|
397
|
+
</Visible>
|
|
398
|
+
```
|
|
399
|
+
|
|
271
400
|
## Development
|
|
272
401
|
|
|
273
402
|
```bash
|
package/README.md
CHANGED
|
@@ -35,6 +35,11 @@ import { Show } from "react-solidlike";
|
|
|
35
35
|
<Show when={user}>
|
|
36
36
|
{(user) => <UserProfile name={user.name} />}
|
|
37
37
|
</Show>
|
|
38
|
+
|
|
39
|
+
// 带 onFallback 回调(用于重定向等副作用)
|
|
40
|
+
<Show when={isAuthenticated} fallback={<Loading />} onFallback={() => navigate('/login')}>
|
|
41
|
+
<Dashboard />
|
|
42
|
+
</Show>
|
|
38
43
|
```
|
|
39
44
|
|
|
40
45
|
### `<For>` - 列表渲染
|
|
@@ -166,6 +171,48 @@ import { Repeat } from "react-solidlike";
|
|
|
166
171
|
</Repeat>
|
|
167
172
|
```
|
|
168
173
|
|
|
174
|
+
### `<Split>` - 字符串切割渲染
|
|
175
|
+
|
|
176
|
+
按分隔符切割字符串并渲染每个部分。
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { Split } from "react-solidlike";
|
|
180
|
+
|
|
181
|
+
// 基础用法 - 切割后不保留分隔符
|
|
182
|
+
<Split string="a,b,c" separator=",">
|
|
183
|
+
{(part) => <span>{part}</span>}
|
|
184
|
+
</Split>
|
|
185
|
+
// 渲染: ["a", "b", "c"]
|
|
186
|
+
|
|
187
|
+
// 保留分隔符
|
|
188
|
+
<Split string="9+5=(9+1)+4" separator="=" keepSeparator>
|
|
189
|
+
{(part) => <span>{part}</span>}
|
|
190
|
+
</Split>
|
|
191
|
+
// 渲染: ["9+5", "=", "(9+1)+4"]
|
|
192
|
+
|
|
193
|
+
// 使用正则表达式分隔符
|
|
194
|
+
<Split string="a1b2c3" separator={/\d/} keepSeparator>
|
|
195
|
+
{(part) => <span>{part}</span>}
|
|
196
|
+
</Split>
|
|
197
|
+
// 渲染: ["a", "1", "b", "2", "c", "3"]
|
|
198
|
+
|
|
199
|
+
// 带 wrapper 包装元素
|
|
200
|
+
<Split string="hello world" separator=" " wrapper={<div className="words" />}>
|
|
201
|
+
{(word) => <span>{word}</span>}
|
|
202
|
+
</Split>
|
|
203
|
+
|
|
204
|
+
// 带 fallback 处理空字符串
|
|
205
|
+
<Split string={text} separator="," fallback={<EmptyState />}>
|
|
206
|
+
{(part) => <Tag>{part}</Tag>}
|
|
207
|
+
</Split>
|
|
208
|
+
|
|
209
|
+
// 倒序渲染
|
|
210
|
+
<Split string="a,b,c" separator="," reverse>
|
|
211
|
+
{(part) => <span>{part}</span>}
|
|
212
|
+
</Split>
|
|
213
|
+
// 渲染顺序: ["c", "b", "a"]
|
|
214
|
+
```
|
|
215
|
+
|
|
169
216
|
### `<Dynamic>` - 动态组件
|
|
170
217
|
|
|
171
218
|
根据条件动态选择要渲染的组件类型。
|
|
@@ -268,6 +315,88 @@ function UserList() {
|
|
|
268
315
|
| `children` | `ReactNode \| (data: T) => ReactNode` | 成功时渲染 |
|
|
269
316
|
| `isEmptyFn` | `(data: T) => boolean` | 自定义空判断 |
|
|
270
317
|
|
|
318
|
+
### `<Once>` - 单次渲染
|
|
319
|
+
|
|
320
|
+
只渲染一次子元素,忽略后续更新。适用于昂贵的计算或不应重新渲染的内容。
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { Once } from "react-solidlike";
|
|
324
|
+
|
|
325
|
+
// 渲染昂贵的组件
|
|
326
|
+
<Once>
|
|
327
|
+
<ExpensiveChart data={data} />
|
|
328
|
+
</Once>
|
|
329
|
+
|
|
330
|
+
// 防止父组件更新导致的重新渲染
|
|
331
|
+
function Parent() {
|
|
332
|
+
const [count, setCount] = useState(0);
|
|
333
|
+
return (
|
|
334
|
+
<div>
|
|
335
|
+
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
|
|
336
|
+
<Once>
|
|
337
|
+
<Child initialCount={count} />
|
|
338
|
+
</Once>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### `<ClientOnly>` - 仅客户端渲染
|
|
345
|
+
|
|
346
|
+
仅在客户端(hydration 之后)渲染子元素。适用于依赖浏览器 API 或需要避免 SSR hydration 不匹配的场景。
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { ClientOnly } from "react-solidlike";
|
|
350
|
+
|
|
351
|
+
// 基础用法
|
|
352
|
+
<ClientOnly>
|
|
353
|
+
<BrowserOnlyComponent />
|
|
354
|
+
</ClientOnly>
|
|
355
|
+
|
|
356
|
+
// 带 SSR 备选内容
|
|
357
|
+
<ClientOnly fallback={<Skeleton />}>
|
|
358
|
+
<DynamicChart />
|
|
359
|
+
</ClientOnly>
|
|
360
|
+
|
|
361
|
+
// 使用渲染函数延迟求值(避免访问 window)
|
|
362
|
+
<ClientOnly fallback={<Loading />}>
|
|
363
|
+
{() => <ComponentUsingWindow width={window.innerWidth} />}
|
|
364
|
+
</ClientOnly>
|
|
365
|
+
|
|
366
|
+
// 避免 hydration 不匹配
|
|
367
|
+
<ClientOnly fallback={<span>--:--</span>}>
|
|
368
|
+
<CurrentTime />
|
|
369
|
+
</ClientOnly>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `<Visible>` - 可见性渲染(仅 Web)
|
|
373
|
+
|
|
374
|
+
基于 IntersectionObserver 的可见性渲染,进入视口才渲染。在 React Native 或不支持的环境中会直接渲染 children(优雅降级)。
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { Visible } from "react-solidlike";
|
|
378
|
+
|
|
379
|
+
// 基础用法 - 进入视口时渲染
|
|
380
|
+
<Visible>
|
|
381
|
+
<HeavyComponent />
|
|
382
|
+
</Visible>
|
|
383
|
+
|
|
384
|
+
// 带占位符
|
|
385
|
+
<Visible fallback={<Skeleton />}>
|
|
386
|
+
<Image src={url} />
|
|
387
|
+
</Visible>
|
|
388
|
+
|
|
389
|
+
// 提前预加载(rootMargin)
|
|
390
|
+
<Visible rootMargin="200px" fallback={<Placeholder />}>
|
|
391
|
+
<LazyImage />
|
|
392
|
+
</Visible>
|
|
393
|
+
|
|
394
|
+
// 切换可见性(once=false 时离开视口会卸载)
|
|
395
|
+
<Visible once={false} onVisibilityChange={(v) => console.log(v)}>
|
|
396
|
+
<VideoPlayer />
|
|
397
|
+
</Visible>
|
|
398
|
+
```
|
|
399
|
+
|
|
271
400
|
## 开发
|
|
272
401
|
|
|
273
402
|
```bash
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface ClientOnlyProps {
|
|
3
|
+
/** Content to render only on client side | 仅在客户端渲染的内容 */
|
|
4
|
+
children: ReactNode | (() => ReactNode);
|
|
5
|
+
/** Fallback content during SSR | SSR 期间渲染的备选内容 */
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Component that renders children only on the client side (after hydration)
|
|
10
|
+
*
|
|
11
|
+
* 仅在客户端(hydration 之后)渲染子元素的组件
|
|
12
|
+
*
|
|
13
|
+
* Useful for components that rely on browser APIs, window object,
|
|
14
|
+
* or need to avoid SSR hydration mismatches.
|
|
15
|
+
*
|
|
16
|
+
* 适用于依赖浏览器 API、window 对象的组件,
|
|
17
|
+
* 或需要避免 SSR hydration 不匹配的场景。
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Basic usage | 基础用法
|
|
21
|
+
* <ClientOnly>
|
|
22
|
+
* <BrowserOnlyComponent />
|
|
23
|
+
* </ClientOnly>
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // With fallback for SSR | 带 SSR 备选内容
|
|
27
|
+
* <ClientOnly fallback={<Skeleton />}>
|
|
28
|
+
* <DynamicChart />
|
|
29
|
+
* </ClientOnly>
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Using render function for lazy evaluation | 使用渲染函数延迟求值
|
|
33
|
+
* <ClientOnly fallback={<Loading />}>
|
|
34
|
+
* {() => <ComponentUsingWindow width={window.innerWidth} />}
|
|
35
|
+
* </ClientOnly>
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Avoid hydration mismatch | 避免 hydration 不匹配
|
|
39
|
+
* <ClientOnly fallback={<span>--:--</span>}>
|
|
40
|
+
* <CurrentTime />
|
|
41
|
+
* </ClientOnly>
|
|
42
|
+
*/
|
|
43
|
+
export declare function ClientOnly({ children, fallback }: ClientOnlyProps): ReactNode;
|
package/dist/Once.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface OnceProps {
|
|
3
|
+
/** Content to render only once | 只渲染一次的内容 */
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Component that renders children only once and ignores subsequent updates
|
|
8
|
+
*
|
|
9
|
+
* 只渲染一次子元素并忽略后续更新的组件
|
|
10
|
+
*
|
|
11
|
+
* Useful for expensive computations or content that should not re-render.
|
|
12
|
+
* The initial render result is cached and returned on subsequent renders.
|
|
13
|
+
*
|
|
14
|
+
* 适用于昂贵的计算或不应重新渲染的内容。
|
|
15
|
+
* 初始渲染结果会被缓存并在后续渲染时返回。
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Render expensive component only once | 只渲染一次昂贵的组件
|
|
19
|
+
* <Once>
|
|
20
|
+
* <ExpensiveChart data={data} />
|
|
21
|
+
* </Once>
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Static content that shouldn't update | 不应更新的静态内容
|
|
25
|
+
* <Once>
|
|
26
|
+
* <Header title={initialTitle} />
|
|
27
|
+
* </Once>
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Prevent re-renders from parent updates | 防止父组件更新导致的重新渲染
|
|
31
|
+
* function Parent() {
|
|
32
|
+
* const [count, setCount] = useState(0);
|
|
33
|
+
* return (
|
|
34
|
+
* <div>
|
|
35
|
+
* <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
|
|
36
|
+
* <Once>
|
|
37
|
+
* <Child initialCount={count} />
|
|
38
|
+
* </Once>
|
|
39
|
+
* </div>
|
|
40
|
+
* );
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
export declare function Once({ children }: OnceProps): ReactNode;
|
package/dist/Repeat.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type ForProps } from './For';
|
|
3
|
+
export interface RepeatProps extends Omit<ForProps<number>, 'each'> {
|
|
3
4
|
/** Number of times to repeat | 重复次数 */
|
|
4
5
|
times: number;
|
|
5
|
-
/** Render function, receives current index and total length | 渲染函数,接收当前索引和总长度 */
|
|
6
|
-
children: (index: number, length: number) => ReactNode;
|
|
7
|
-
/** Wrapper element for all rendered elements | 包装所有渲染元素的元素 */
|
|
8
|
-
wrapper?: ReactElement;
|
|
9
|
-
/** Reverse the rendering order | 倒序渲染 */
|
|
10
|
-
reverse?: boolean;
|
|
11
6
|
}
|
|
12
7
|
/**
|
|
13
8
|
* Repeat rendering component, replaces Array.from({ length: n }).map()
|
|
@@ -38,4 +33,4 @@ export interface RepeatProps {
|
|
|
38
33
|
* {(i) => <Star key={i} />}
|
|
39
34
|
* </Repeat>
|
|
40
35
|
*/
|
|
41
|
-
export declare function Repeat({ times, children,
|
|
36
|
+
export declare function Repeat({ times, children, ...props }: RepeatProps): ReactNode;
|
package/dist/Show.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface ShowProps<T> {
|
|
|
6
6
|
children: ReactNode | ((value: NonNullable<T>) => ReactNode);
|
|
7
7
|
/** Fallback content when condition is falsy | 条件为假时渲染的备选内容 */
|
|
8
8
|
fallback?: ReactNode;
|
|
9
|
+
/** Callback when condition is falsy (called before rendering fallback) | 条件为假时的回调(在渲染 fallback 之前调用) */
|
|
10
|
+
onFallback?: () => void;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Conditional rendering component, replaces ternary expressions in JSX
|
|
@@ -29,5 +31,11 @@ export interface ShowProps<T> {
|
|
|
29
31
|
* <Show when={user}>
|
|
30
32
|
* {(user) => <UserProfile name={user.name} />}
|
|
31
33
|
* </Show>
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // With onFallback callback for side effects | 带 onFallback 回调用于副作用
|
|
37
|
+
* <Show when={isAuthenticated} fallback={<Loading />} onFallback={() => navigate('/login')}>
|
|
38
|
+
* <Dashboard />
|
|
39
|
+
* </Show>
|
|
32
40
|
*/
|
|
33
|
-
export declare function Show<T>({ when, children, fallback }: ShowProps<T>): ReactNode;
|
|
41
|
+
export declare function Show<T>({ when, children, fallback, onFallback }: ShowProps<T>): ReactNode;
|
package/dist/Split.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type ForProps } from './For';
|
|
3
|
+
export interface SplitProps extends Omit<ForProps<string>, 'each'> {
|
|
4
|
+
/** String to split | 要切割的字符串 */
|
|
5
|
+
string: string | null | undefined;
|
|
6
|
+
/** Separator to split by, can be string or RegExp | 分隔符,可以是字符串或正则表达式 */
|
|
7
|
+
separator: string | RegExp;
|
|
8
|
+
/** Whether to keep separator in result array | 是否在结果数组中保留分隔符 */
|
|
9
|
+
keepSeparator?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* String splitting and rendering component, splits a string by separator and renders each part
|
|
13
|
+
*
|
|
14
|
+
* 字符串切割渲染组件,按分隔符切割字符串并渲染每个部分
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Basic usage | 基础用法
|
|
18
|
+
* <Split string="a,b,c" separator=",">
|
|
19
|
+
* {(part) => <span>{part}</span>}
|
|
20
|
+
* </Split>
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Keep separator | 保留分隔符
|
|
24
|
+
* <Split string="9+5=(9+1)+4" separator="=" keepSeparator>
|
|
25
|
+
* {(part) => <span>{part}</span>}
|
|
26
|
+
* </Split>
|
|
27
|
+
* // Renders: ["9+5", "=", "(9+1)+4"]
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Without keeping separator | 不保留分隔符
|
|
31
|
+
* <Split string="9+5=(9+1)+4" separator="=">
|
|
32
|
+
* {(part) => <span>{part}</span>}
|
|
33
|
+
* </Split>
|
|
34
|
+
* // Renders: ["9+5", "(9+1)+4"]
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // With RegExp separator | 使用正则表达式分隔符
|
|
38
|
+
* <Split string="a1b2c3" separator={/\d/} keepSeparator>
|
|
39
|
+
* {(part) => <span>{part}</span>}
|
|
40
|
+
* </Split>
|
|
41
|
+
* // Renders: ["a", "1", "b", "2", "c", "3"]
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // With wrapper element | 使用包装元素
|
|
45
|
+
* <Split string="hello world" separator=" " wrapper={<div className="words" />}>
|
|
46
|
+
* {(word) => <span>{word}</span>}
|
|
47
|
+
* </Split>
|
|
48
|
+
*/
|
|
49
|
+
export declare function Split({ string, separator, keepSeparator, children, ...props }: SplitProps): ReactNode;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface VisibleProps {
|
|
3
|
+
/** Content to render when visible | 可见时渲染的内容 */
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Fallback content before entering viewport | 进入视口前渲染的备选内容 */
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
/** Root margin for intersection observer (e.g., "100px") | 交叉观察器的根边距 */
|
|
8
|
+
rootMargin?: string;
|
|
9
|
+
/** Visibility threshold (0-1) | 可见性阈值 */
|
|
10
|
+
threshold?: number | number[];
|
|
11
|
+
/** Keep rendered after first visible (default: true) | 首次可见后保持渲染 */
|
|
12
|
+
once?: boolean;
|
|
13
|
+
/** Callback when visibility changes | 可见性变化时的回调 */
|
|
14
|
+
onVisibilityChange?: (isVisible: boolean) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Visibility-based rendering component using IntersectionObserver (Web only)
|
|
18
|
+
*
|
|
19
|
+
* 基于可见性的渲染组件,使用 IntersectionObserver(仅 Web)
|
|
20
|
+
*
|
|
21
|
+
* In React Native or environments without IntersectionObserver,
|
|
22
|
+
* children will be rendered directly (graceful degradation).
|
|
23
|
+
*
|
|
24
|
+
* 在 React Native 或不支持 IntersectionObserver 的环境中,
|
|
25
|
+
* 会直接渲染 children(优雅降级)。
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Basic usage - render when entering viewport | 基础用法 - 进入视口时渲染
|
|
29
|
+
* <Visible>
|
|
30
|
+
* <HeavyComponent />
|
|
31
|
+
* </Visible>
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // With fallback placeholder | 带占位符
|
|
35
|
+
* <Visible fallback={<Skeleton />}>
|
|
36
|
+
* <Image src={url} />
|
|
37
|
+
* </Visible>
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Preload before entering viewport | 提前预加载
|
|
41
|
+
* <Visible rootMargin="200px" fallback={<Placeholder />}>
|
|
42
|
+
* <LazyImage />
|
|
43
|
+
* </Visible>
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Toggle visibility (once=false) | 切换可见性
|
|
47
|
+
* <Visible once={false} onVisibilityChange={(v) => console.log(v)}>
|
|
48
|
+
* <VideoPlayer />
|
|
49
|
+
* </Visible>
|
|
50
|
+
*/
|
|
51
|
+
export declare function Visible({ children, fallback, rootMargin, threshold, once, onVisibilityChange, }: VisibleProps): ReactNode;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export { Await, type AwaitProps } from "./Await";
|
|
2
|
+
export { ClientOnly, type ClientOnlyProps } from "./ClientOnly";
|
|
2
3
|
export { Dynamic, type DynamicProps } from "./Dynamic";
|
|
3
4
|
export { ErrorBoundary, type ErrorBoundaryProps } from "./ErrorBoundary";
|
|
4
5
|
export { For, type ForProps } from "./For";
|
|
6
|
+
export { Once, type OnceProps } from "./Once";
|
|
5
7
|
export { QueryBoundary, type QueryBoundaryProps, type QueryResult } from "./QueryBoundary";
|
|
6
8
|
export { Repeat, type RepeatProps } from "./Repeat";
|
|
7
9
|
export { Show, type ShowProps } from "./Show";
|
|
10
|
+
export { Split, type SplitProps } from "./Split";
|
|
8
11
|
export { Default, type DefaultProps, Match, type MatchProps, Switch, type SwitchProps } from "./Switch";
|
|
12
|
+
export { Visible, type VisibleProps } from "./Visible";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Children, Component, Fragment, cloneElement, createElement, isValidElement, useEffect, useState } from "react";
|
|
1
|
+
import { Children, Component, Fragment, cloneElement, createElement, isValidElement, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
function Await({ promise, loading = null, error = null, children }) {
|
|
@@ -43,6 +43,16 @@ function Await({ promise, loading = null, error = null, children }) {
|
|
|
43
43
|
return children;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function ClientOnly({ children, fallback = null }) {
|
|
47
|
+
const [isClient, setIsClient] = useState(false);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setIsClient(true);
|
|
50
|
+
}, []);
|
|
51
|
+
if (!isClient) return fallback;
|
|
52
|
+
if (typeof children === "function") return children();
|
|
53
|
+
return children;
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
function Dynamic({ component, fallback = null, ...props }) {
|
|
47
57
|
if (!component) return fallback;
|
|
48
58
|
return createElement(component, props);
|
|
@@ -86,6 +96,18 @@ function For({ each, children, keyExtractor, fallback = null, wrapper, reverse }
|
|
|
86
96
|
return wrapper && isValidElement(wrapper) ? cloneElement(wrapper, {}, elements) : elements;
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
function Once({ children }) {
|
|
100
|
+
const cachedRef = useRef({
|
|
101
|
+
rendered: false,
|
|
102
|
+
content: null
|
|
103
|
+
});
|
|
104
|
+
if (!cachedRef.current.rendered) {
|
|
105
|
+
cachedRef.current.rendered = true;
|
|
106
|
+
cachedRef.current.content = children;
|
|
107
|
+
}
|
|
108
|
+
return cachedRef.current.content;
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
function defaultIsEmpty(data) {
|
|
90
112
|
if (data == null) return true;
|
|
91
113
|
if (Array.isArray(data)) return data.length === 0;
|
|
@@ -103,16 +125,20 @@ function QueryBoundary({ query, loading = null, error = null, empty = null, chil
|
|
|
103
125
|
return children;
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
function Repeat({ times, children,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
function Repeat({ times, children, ...props }) {
|
|
129
|
+
const indices = times > 0 ? Array.from({ length: times }, (_, i) => i) : [];
|
|
130
|
+
return /* @__PURE__ */ jsx(For, {
|
|
131
|
+
...props,
|
|
132
|
+
each: indices,
|
|
133
|
+
children: (i, _i, array) => children(i, array.length, array)
|
|
134
|
+
});
|
|
112
135
|
}
|
|
113
136
|
|
|
114
|
-
function Show({ when, children, fallback = null }) {
|
|
115
|
-
if (!when || isEmpty(when))
|
|
137
|
+
function Show({ when, children, fallback = null, onFallback }) {
|
|
138
|
+
if (!when || isEmpty(when)) {
|
|
139
|
+
onFallback?.();
|
|
140
|
+
return fallback;
|
|
141
|
+
}
|
|
116
142
|
if (typeof children === "function") return children(when);
|
|
117
143
|
return children;
|
|
118
144
|
}
|
|
@@ -123,6 +149,28 @@ function isEmpty(value) {
|
|
|
123
149
|
return false;
|
|
124
150
|
}
|
|
125
151
|
|
|
152
|
+
function Split({ string, separator, keepSeparator = false, children, ...props }) {
|
|
153
|
+
const parts = splitString(string, separator, keepSeparator);
|
|
154
|
+
return /* @__PURE__ */ jsx(For, {
|
|
155
|
+
...props,
|
|
156
|
+
each: parts,
|
|
157
|
+
children
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function splitString(string, separator, keepSeparator) {
|
|
162
|
+
if (!string) return [];
|
|
163
|
+
if (keepSeparator) {
|
|
164
|
+
const regex = separator instanceof RegExp ? new RegExp(`(${separator.source})`, separator.flags.includes("g") ? separator.flags : `${separator.flags}g`) : new RegExp(`(${escapeRegExp(separator)})`, "g");
|
|
165
|
+
return string.split(regex).filter((part) => part !== "");
|
|
166
|
+
}
|
|
167
|
+
return string.split(separator);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function escapeRegExp(str) {
|
|
171
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
172
|
+
}
|
|
173
|
+
|
|
126
174
|
function Match(_props) {
|
|
127
175
|
return null;
|
|
128
176
|
}
|
|
@@ -158,4 +206,55 @@ function Switch({ children, fallback = null }) {
|
|
|
158
206
|
return defaultContent;
|
|
159
207
|
}
|
|
160
208
|
|
|
161
|
-
|
|
209
|
+
function Visible({ children, fallback = null, rootMargin = "0px", threshold = 0, once = true, onVisibilityChange }) {
|
|
210
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
211
|
+
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
|
212
|
+
const [isClient, setIsClient] = useState(false);
|
|
213
|
+
const ref = useRef(null);
|
|
214
|
+
const isSupported = typeof window !== "undefined" && typeof IntersectionObserver !== "undefined";
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
setIsClient(true);
|
|
217
|
+
}, []);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!isClient) return;
|
|
220
|
+
if (!isSupported) {
|
|
221
|
+
setIsVisible(true);
|
|
222
|
+
setHasBeenVisible(true);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const element = ref.current;
|
|
226
|
+
if (!element) return;
|
|
227
|
+
const observer = new IntersectionObserver((entries) => {
|
|
228
|
+
const entry = entries[0];
|
|
229
|
+
if (entry) {
|
|
230
|
+
const visible = entry.isIntersecting;
|
|
231
|
+
setIsVisible(visible);
|
|
232
|
+
onVisibilityChange?.(visible);
|
|
233
|
+
if (visible) {
|
|
234
|
+
setHasBeenVisible(true);
|
|
235
|
+
if (once) observer.disconnect();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}, {
|
|
239
|
+
rootMargin,
|
|
240
|
+
threshold
|
|
241
|
+
});
|
|
242
|
+
observer.observe(element);
|
|
243
|
+
return () => observer.disconnect();
|
|
244
|
+
}, [
|
|
245
|
+
isClient,
|
|
246
|
+
isSupported,
|
|
247
|
+
rootMargin,
|
|
248
|
+
threshold,
|
|
249
|
+
once,
|
|
250
|
+
onVisibilityChange
|
|
251
|
+
]);
|
|
252
|
+
if (!isClient) return children;
|
|
253
|
+
if (!isSupported) return children;
|
|
254
|
+
return /* @__PURE__ */ jsx("div", {
|
|
255
|
+
ref,
|
|
256
|
+
children: (once ? hasBeenVisible : isVisible) ? children : fallback
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { Await, ClientOnly, Default, Dynamic, ErrorBoundary, For, Match, Once, QueryBoundary, Repeat, Show, Split, Switch, Visible };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-solidlike",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Declarative React control flow components inspired by Solid.js, replacing ternary expressions and array.map() in JSX",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "rm -rf dist && rolldown -c && tsc -p tsconfig.build.json",
|
|
20
20
|
"test": "bun test",
|
|
21
|
+
"test:e2e": "playwright test",
|
|
21
22
|
"lint": "biome check .",
|
|
22
23
|
"lint:fix": "biome check --write .",
|
|
23
24
|
"format": "biome format --write .",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@biomejs/biome": "^2.3.11",
|
|
45
46
|
"@happy-dom/global-registrator": "^20.1.0",
|
|
47
|
+
"@playwright/test": "^1.58.0",
|
|
46
48
|
"@testing-library/react": "^16.3.1",
|
|
47
49
|
"@testing-library/user-event": "^14.6.1",
|
|
48
50
|
"@types/bun": "latest",
|