@diskette/use-render 0.9.0 → 0.10.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.md +38 -25
- package/dist/fns/index.d.ts +2 -0
- package/dist/fns/index.js +1 -0
- package/dist/fns/render-slot.d.ts +36 -0
- package/dist/fns/render-slot.js +52 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -8,11 +8,35 @@ pnpm add @diskette/use-render
|
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
10
|
|
|
11
|
-
When building component libraries, you often need to let consumers customize rendering
|
|
11
|
+
When building component libraries, you often need to let consumers customize rendering to:
|
|
12
|
+
|
|
13
|
+
- swap the underlying element
|
|
14
|
+
- access internal state for styling
|
|
15
|
+
- compose refs and event handlers.
|
|
16
|
+
|
|
17
|
+
These hooks handle that plumbing:
|
|
12
18
|
|
|
13
19
|
- **Component authors** get prop merging, ref composition, and render prop support out of the box
|
|
14
20
|
- **Consumers** get type-safe APIs where `className`, `style`, and `children` can be functions of component state
|
|
15
21
|
|
|
22
|
+
## `useRender` — Stateful Components
|
|
23
|
+
|
|
24
|
+
For components that expose internal state to consumers. The state flows through `className`, `style`, `children`, and `render` as callback parameters.
|
|
25
|
+
|
|
26
|
+
**Component author:**
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { useRender, ComponentProps } from '@diskette/use-render'
|
|
30
|
+
|
|
31
|
+
type State = { disabled: boolean; loading: boolean }
|
|
32
|
+
type ButtonProps = ComponentProps<'button', State>
|
|
33
|
+
|
|
34
|
+
function Button(props: ButtonProps) {
|
|
35
|
+
const state: State = { disabled: props.disabled ?? false, loading: false }
|
|
36
|
+
return useRender('button', state, { props })
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
16
40
|
## `useRenderSlot` — Stateless Slots
|
|
17
41
|
|
|
18
42
|
For wrapper components that don't expose internal state. Consumers can still swap the element or pass props—they just won't get state callbacks.
|
|
@@ -39,24 +63,6 @@ function Card(props: CardProps) {
|
|
|
39
63
|
|
|
40
64
|
Props merge automatically—`className` and `style` combine, refs compose, event handlers chain (consumer runs first). The `render` prop receives only `props` since there's no state to pass.
|
|
41
65
|
|
|
42
|
-
## `useRender` — Stateful Components
|
|
43
|
-
|
|
44
|
-
For components that expose internal state to consumers. The state flows through `className`, `style`, `children`, and `render` as callback parameters.
|
|
45
|
-
|
|
46
|
-
**Component author:**
|
|
47
|
-
|
|
48
|
-
```tsx
|
|
49
|
-
import { useRender, ComponentProps } from '@diskette/use-render'
|
|
50
|
-
|
|
51
|
-
type State = { disabled: boolean; loading: boolean }
|
|
52
|
-
type ButtonProps = ComponentProps<'button', State>
|
|
53
|
-
|
|
54
|
-
function Button(props: ButtonProps) {
|
|
55
|
-
const state: State = { disabled: props.disabled ?? false, loading: false }
|
|
56
|
-
return useRender('button', state, { props })
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
66
|
**Consumer:**
|
|
61
67
|
|
|
62
68
|
```tsx
|
|
@@ -101,7 +107,11 @@ function List({ items, ...props }: ListProps & { items: string[] }) {
|
|
|
101
107
|
{ props, baseProps: { children: (item) => <li>{item.value}</li> } },
|
|
102
108
|
)
|
|
103
109
|
// containerProps provides direct access to resolved props if needed
|
|
104
|
-
return
|
|
110
|
+
return (
|
|
111
|
+
<Container>
|
|
112
|
+
{items.map((v, i) => renderItem({ index: i, value: v }))}
|
|
113
|
+
</Container>
|
|
114
|
+
)
|
|
105
115
|
}
|
|
106
116
|
```
|
|
107
117
|
|
|
@@ -122,10 +132,10 @@ function List({ items, ...props }: ListProps & { items: string[] }) {
|
|
|
122
132
|
|
|
123
133
|
Each hook has a corresponding type for your component's public API:
|
|
124
134
|
|
|
125
|
-
| Hook
|
|
126
|
-
|
|
127
|
-
| `useRenderSlot`
|
|
128
|
-
| `useRender`
|
|
135
|
+
| Hook | Props Type | State |
|
|
136
|
+
| -------------------- | --------------------------- | ---------------- |
|
|
137
|
+
| `useRenderSlot` | `SlotProps<T>` | None |
|
|
138
|
+
| `useRender` | `ComponentProps<T, S>` | Single state |
|
|
129
139
|
| `useRenderContainer` | `ContainerProps<T, CS, IS>` | Container + Item |
|
|
130
140
|
|
|
131
141
|
These extend the element's native props, adding `render` and (for stateful hooks) function forms of `className`, `style`, and `children`.
|
|
@@ -155,7 +165,10 @@ export interface ComboboxRef {
|
|
|
155
165
|
clear: () => void
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
function Combobox({
|
|
168
|
+
function Combobox({
|
|
169
|
+
ref,
|
|
170
|
+
...props
|
|
171
|
+
}: ComboboxProps & { ref?: React.Ref<ComboboxRef> }) {
|
|
159
172
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
160
173
|
const state: State = { open: false }
|
|
161
174
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderSlot } from "./render-slot.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CSSProperties, ElementType, HTMLAttributes, JSX, ReactNode } from 'react';
|
|
2
|
+
import type { DataAttributes } from '../types.ts';
|
|
3
|
+
export type SlotRenderer = (props: HTMLAttributes<any>) => ReactNode;
|
|
4
|
+
export type SlotProps<T extends ElementType> = Omit<React.ComponentProps<T>, 'className' | 'style' | 'ref'> & {
|
|
5
|
+
className?: string | undefined;
|
|
6
|
+
style?: CSSProperties | undefined;
|
|
7
|
+
render?: SlotRenderer | JSX.Element;
|
|
8
|
+
};
|
|
9
|
+
export interface RenderSlotOptions<T extends ElementType> {
|
|
10
|
+
baseProps?: Omit<React.ComponentProps<T>, 'ref'> & DataAttributes;
|
|
11
|
+
props?: SlotProps<T> & DataAttributes;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Pure function for rendering slot elements with render prop support and prop merging.
|
|
15
|
+
* RSC-compatible version of useRenderSlot without ref handling.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { renderSlot, SlotProps } from '@diskette/use-render/fns'
|
|
20
|
+
*
|
|
21
|
+
* type CardProps = SlotProps<'div'>
|
|
22
|
+
*
|
|
23
|
+
* function Card(props: CardProps) {
|
|
24
|
+
* return renderSlot('div', {
|
|
25
|
+
* props,
|
|
26
|
+
* baseProps: { className: 'card' },
|
|
27
|
+
* })
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* // Usage:
|
|
31
|
+
* <Card className="card-primary" />
|
|
32
|
+
* <Card render={<section />} />
|
|
33
|
+
* <Card render={(props) => <article {...props} />} />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderSlot<T extends ElementType>(tag: T, options?: RenderSlotOptions<T>): ReactNode;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cloneElement, createElement, isValidElement } from 'react';
|
|
2
|
+
import { cx, isFunction, isString, mergeProps } from "../utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pure function for rendering slot elements with render prop support and prop merging.
|
|
5
|
+
* RSC-compatible version of useRenderSlot without ref handling.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { renderSlot, SlotProps } from '@diskette/use-render/fns'
|
|
10
|
+
*
|
|
11
|
+
* type CardProps = SlotProps<'div'>
|
|
12
|
+
*
|
|
13
|
+
* function Card(props: CardProps) {
|
|
14
|
+
* return renderSlot('div', {
|
|
15
|
+
* props,
|
|
16
|
+
* baseProps: { className: 'card' },
|
|
17
|
+
* })
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* // Usage:
|
|
21
|
+
* <Card className="card-primary" />
|
|
22
|
+
* <Card render={<section />} />
|
|
23
|
+
* <Card render={(props) => <article {...props} />} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function renderSlot(tag, options = {}) {
|
|
27
|
+
const baseProps = (options.baseProps ?? {});
|
|
28
|
+
const props = (options.props ?? {});
|
|
29
|
+
const { className: baseClassName, style: baseStyle, children: baseChildren, ...base } = baseProps;
|
|
30
|
+
const { className, style, children, render, ...rest } = props;
|
|
31
|
+
const resolvedClassName = cx(baseClassName, className);
|
|
32
|
+
const resolvedStyle = baseStyle || style ? { ...baseStyle, ...style } : undefined;
|
|
33
|
+
const resolvedProps = {
|
|
34
|
+
...mergeProps(base, rest),
|
|
35
|
+
};
|
|
36
|
+
if (isString(resolvedClassName)) {
|
|
37
|
+
resolvedProps.className = resolvedClassName;
|
|
38
|
+
}
|
|
39
|
+
if (typeof resolvedStyle === 'object') {
|
|
40
|
+
resolvedProps.style = resolvedStyle;
|
|
41
|
+
}
|
|
42
|
+
const resolvedChildren = children ?? baseChildren;
|
|
43
|
+
// For `<Component render={<a />} />`
|
|
44
|
+
if (isValidElement(render)) {
|
|
45
|
+
return cloneElement(render, resolvedProps, resolvedChildren);
|
|
46
|
+
}
|
|
47
|
+
// For `<Component render={(props) => <a {...props} />)} />`
|
|
48
|
+
if (isFunction(render)) {
|
|
49
|
+
return render({ ...resolvedProps, children: resolvedChildren });
|
|
50
|
+
}
|
|
51
|
+
return createElement(tag, resolvedProps, resolvedChildren);
|
|
52
|
+
}
|
package/package.json
CHANGED