@diskette/use-render 0.3.0 → 0.5.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 +15 -15
- package/dist/types.d.ts +4 -4
- package/dist/use-render.d.ts +19 -3
- package/dist/use-render.js +27 -11
- package/dist/use-render.test.jsx +10 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ function Button(props: ButtonProps) {
|
|
|
39
39
|
const state: ButtonState = { isPressed, isHovered }
|
|
40
40
|
|
|
41
41
|
return useRender('button', state, {
|
|
42
|
-
|
|
42
|
+
baseProps: {
|
|
43
43
|
className: 'btn',
|
|
44
44
|
onMouseDown: () => setIsPressed(true),
|
|
45
45
|
onMouseUp: () => setIsPressed(false),
|
|
@@ -69,7 +69,7 @@ function Button(props: ButtonProps) {
|
|
|
69
69
|
|
|
70
70
|
```tsx
|
|
71
71
|
<Button
|
|
72
|
-
render={(
|
|
72
|
+
render={(props, state) => (
|
|
73
73
|
<a {...props} href="/path" data-pressed={state.isPressed} />
|
|
74
74
|
)}
|
|
75
75
|
>
|
|
@@ -83,8 +83,8 @@ Pass a function to resolve className based on component state:
|
|
|
83
83
|
|
|
84
84
|
```tsx
|
|
85
85
|
<Button
|
|
86
|
-
className={(state,
|
|
87
|
-
`${
|
|
86
|
+
className={(state, baseClassName) =>
|
|
87
|
+
`${baseClassName} ${state.isPressed ? 'pressed' : ''}`
|
|
88
88
|
}
|
|
89
89
|
>
|
|
90
90
|
Press me
|
|
@@ -141,17 +141,17 @@ function useRender<T extends ElementType, S>(
|
|
|
141
141
|
|
|
142
142
|
```ts
|
|
143
143
|
interface UseRenderOptions<T extends ElementType, S> {
|
|
144
|
-
|
|
144
|
+
baseProps?: React.ComponentProps<T>
|
|
145
145
|
props?: ComponentProps<T, S>
|
|
146
146
|
ref?: React.Ref<any> | (React.Ref<any> | undefined)[]
|
|
147
147
|
}
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
| Option
|
|
151
|
-
|
|
|
152
|
-
| `
|
|
153
|
-
| `props`
|
|
154
|
-
| `ref`
|
|
150
|
+
| Option | Description |
|
|
151
|
+
| ----------- | ------------------------------------------------------------------ |
|
|
152
|
+
| `baseProps` | Base props applied to the element |
|
|
153
|
+
| `props` | Consumer-provided props (typically forwarded from component props) |
|
|
154
|
+
| `ref` | Ref(s) to merge with the consumer's ref |
|
|
155
155
|
|
|
156
156
|
### `ComponentProps<T, S>`
|
|
157
157
|
|
|
@@ -170,7 +170,7 @@ type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
|
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
172
|
type ClassNameResolver<S> =
|
|
173
|
-
| ((state: S,
|
|
173
|
+
| ((state: S, baseClassName?: string) => string | undefined)
|
|
174
174
|
| string
|
|
175
175
|
| undefined
|
|
176
176
|
```
|
|
@@ -179,17 +179,17 @@ type ClassNameResolver<S> =
|
|
|
179
179
|
|
|
180
180
|
```ts
|
|
181
181
|
type StyleResolver<S> =
|
|
182
|
-
| ((state: S,
|
|
182
|
+
| ((state: S, baseStyle?: CSSProperties) => CSSProperties | undefined)
|
|
183
183
|
| CSSProperties
|
|
184
184
|
| undefined
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
### `Renderer<
|
|
187
|
+
### `Renderer<S>`
|
|
188
188
|
|
|
189
189
|
```ts
|
|
190
|
-
type Renderer<
|
|
191
|
-
state: S,
|
|
190
|
+
type Renderer<S> = (
|
|
192
191
|
props: ComponentPropsWithRef<T>,
|
|
192
|
+
state: S,
|
|
193
193
|
) => ReactNode
|
|
194
194
|
```
|
|
195
195
|
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactNode } from 'react';
|
|
2
|
-
export type ClassNameResolver<State> = ((state: State,
|
|
3
|
-
export type StyleResolver<State> = ((state: State,
|
|
4
|
-
export type Renderer<S> = (
|
|
2
|
+
export type ClassNameResolver<State> = ((state: State, baseClassName?: string) => string | undefined) | string | undefined;
|
|
3
|
+
export type StyleResolver<State> = ((state: State, baseStyle?: CSSProperties) => CSSProperties | undefined) | CSSProperties | undefined;
|
|
4
|
+
export type Renderer<S> = (props: React.HTMLAttributes<any> & {
|
|
5
5
|
ref?: React.Ref<any> | undefined;
|
|
6
|
-
}) => ReactNode;
|
|
6
|
+
}, state: S) => ReactNode;
|
|
7
7
|
export type DataAttributes = Record<`data-${string}`, string | number | boolean>;
|
|
8
8
|
export type BaseComponentProps<T extends ElementType> = Omit<ComponentPropsWithRef<T>, 'children' | 'className' | 'style'>;
|
package/dist/use-render.d.ts
CHANGED
|
@@ -7,12 +7,28 @@ export type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
|
|
|
7
7
|
render?: Renderer<S> | JSX.Element;
|
|
8
8
|
};
|
|
9
9
|
export interface UseRenderOptions<T extends ElementType, S> {
|
|
10
|
-
|
|
10
|
+
baseProps?: React.ComponentProps<T> & DataAttributes;
|
|
11
11
|
props?: ComponentProps<T, S> & DataAttributes;
|
|
12
12
|
ref?: React.Ref<any> | (React.Ref<any> | undefined)[];
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Hook for
|
|
16
|
-
*
|
|
15
|
+
* Hook for rendering elements with render prop support, prop merging, and state-driven className/style resolution.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { useRender, ComponentProps } from '@diskette/use-render'
|
|
20
|
+
*
|
|
21
|
+
* type State = { disabled: boolean; loading: boolean }
|
|
22
|
+
* type ButtonProps = ComponentProps<'button', State>
|
|
23
|
+
*
|
|
24
|
+
* function Button(props: ButtonProps) {
|
|
25
|
+
* const state: State = { disabled: props.disabled ?? false, loading: false }
|
|
26
|
+
* return useRender('button', state, { props })
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* // Usage:
|
|
30
|
+
* <Button className={(state) => state.disabled && 'opacity-50'} />
|
|
31
|
+
* <Button render={<a href="#" />} />
|
|
32
|
+
* ```
|
|
17
33
|
*/
|
|
18
34
|
export declare function useRender<T extends ElementType, S>(tag: T, state: S, options: UseRenderOptions<T, S>): ReactNode;
|
package/dist/use-render.js
CHANGED
|
@@ -2,24 +2,40 @@ import { cloneElement, createElement, isValidElement } from 'react';
|
|
|
2
2
|
import { useComposedRef } from "./use-composed-ref.js";
|
|
3
3
|
import { isFunction, isString, mergeProps, resolveClassName, resolveStyle, } from "./utils.js";
|
|
4
4
|
/**
|
|
5
|
-
* Hook for
|
|
6
|
-
*
|
|
5
|
+
* Hook for rendering elements with render prop support, prop merging, and state-driven className/style resolution.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { useRender, ComponentProps } from '@diskette/use-render'
|
|
10
|
+
*
|
|
11
|
+
* type State = { disabled: boolean; loading: boolean }
|
|
12
|
+
* type ButtonProps = ComponentProps<'button', State>
|
|
13
|
+
*
|
|
14
|
+
* function Button(props: ButtonProps) {
|
|
15
|
+
* const state: State = { disabled: props.disabled ?? false, loading: false }
|
|
16
|
+
* return useRender('button', state, { props })
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // Usage:
|
|
20
|
+
* <Button className={(state) => state.disabled && 'opacity-50'} />
|
|
21
|
+
* <Button render={<a href="#" />} />
|
|
22
|
+
* ```
|
|
7
23
|
*/
|
|
8
24
|
export function useRender(tag, state, options) {
|
|
9
25
|
// Workarounds for getting the prop objects to be typed. But should still be ok as the properties we need is common to all elements
|
|
10
|
-
const
|
|
26
|
+
const baseProps = options.baseProps ?? {};
|
|
11
27
|
const props = (options.props ?? {});
|
|
12
|
-
const { className:
|
|
28
|
+
const { className: baseClassName, style: baseStyle, children: baseChildren, ...base } = baseProps;
|
|
13
29
|
const { className, style, children, ref, render, ...rest } = props ?? {};
|
|
14
|
-
const resolvedClassName = resolveClassName(
|
|
15
|
-
const resolvedStyle = resolveStyle(
|
|
30
|
+
const resolvedClassName = resolveClassName(baseClassName, className, state);
|
|
31
|
+
const resolvedStyle = resolveStyle(baseStyle, style, state);
|
|
16
32
|
const refs = Array.isArray(options.ref)
|
|
17
33
|
? [ref, ...options.ref]
|
|
18
34
|
: [ref, options.ref];
|
|
19
35
|
const mergedRef = useComposedRef(refs);
|
|
20
36
|
// Another workaround for getting component props typed
|
|
21
37
|
const resolvedProps = {
|
|
22
|
-
...mergeProps(
|
|
38
|
+
...mergeProps(base, rest),
|
|
23
39
|
ref: mergedRef,
|
|
24
40
|
};
|
|
25
41
|
if (isString(resolvedClassName)) {
|
|
@@ -33,12 +49,12 @@ export function useRender(tag, state, options) {
|
|
|
33
49
|
if (isValidElement(render)) {
|
|
34
50
|
return cloneElement(render, resolvedProps, resolvedChildren);
|
|
35
51
|
}
|
|
36
|
-
// For `<Component render={(
|
|
52
|
+
// For `<Component render={(props) => <a {...props} />)} />`
|
|
37
53
|
if (isFunction(render)) {
|
|
38
|
-
return render(
|
|
54
|
+
return render({
|
|
39
55
|
...resolvedProps,
|
|
40
56
|
children: resolvedChildren,
|
|
41
|
-
});
|
|
57
|
+
}, state);
|
|
42
58
|
}
|
|
43
|
-
return createElement(tag, resolvedProps, resolvedChildren ??
|
|
59
|
+
return createElement(tag, resolvedProps, resolvedChildren ?? baseChildren);
|
|
44
60
|
}
|
package/dist/use-render.test.jsx
CHANGED
|
@@ -8,7 +8,7 @@ function createBtn(defaultProps) {
|
|
|
8
8
|
const state = { isActive };
|
|
9
9
|
return useRender('button', state, {
|
|
10
10
|
props,
|
|
11
|
-
|
|
11
|
+
baseProps: {
|
|
12
12
|
onClick: () => setIsActive((cur) => !cur),
|
|
13
13
|
...defaultProps,
|
|
14
14
|
},
|
|
@@ -33,9 +33,9 @@ describe('useRender', () => {
|
|
|
33
33
|
await button.click();
|
|
34
34
|
await expect.element(button).toHaveClass('active');
|
|
35
35
|
});
|
|
36
|
-
test('className function receives
|
|
36
|
+
test('className function receives baseClassName as second argument', async () => {
|
|
37
37
|
const Button = createBtn({ className: 'btn-default' });
|
|
38
|
-
const { getByRole } = await render(<Button className={(_state,
|
|
38
|
+
const { getByRole } = await render(<Button className={(_state, baseClassName) => `custom ${baseClassName ?? ''}`}>
|
|
39
39
|
Click
|
|
40
40
|
</Button>);
|
|
41
41
|
const button = getByRole('button');
|
|
@@ -69,10 +69,10 @@ describe('useRender', () => {
|
|
|
69
69
|
await button.click();
|
|
70
70
|
await expect.element(button).toHaveStyle({ backgroundColor: 'green' });
|
|
71
71
|
});
|
|
72
|
-
test('style function receives
|
|
72
|
+
test('style function receives baseStyle as second argument', async () => {
|
|
73
73
|
const Button = createBtn({ style: { padding: '10px' } });
|
|
74
|
-
const { getByRole } = await render(<Button style={(_state,
|
|
75
|
-
...
|
|
74
|
+
const { getByRole } = await render(<Button style={(_state, baseStyle) => ({
|
|
75
|
+
...baseStyle,
|
|
76
76
|
color: 'blue',
|
|
77
77
|
})}>
|
|
78
78
|
Click
|
|
@@ -126,9 +126,9 @@ describe('useRender', () => {
|
|
|
126
126
|
await expect.element(link).toHaveTextContent('Link');
|
|
127
127
|
await expect.element(link).toHaveAttribute('href', '/path');
|
|
128
128
|
});
|
|
129
|
-
test('render function receives
|
|
129
|
+
test('render function receives props and state', async () => {
|
|
130
130
|
const Button = createBtn();
|
|
131
|
-
const { getByRole } = await render(<Button className="custom" render={(
|
|
131
|
+
const { getByRole } = await render(<Button className="custom" render={(props, state) => (<a {...props} href="/path" data-active={state.isActive} onClick={(e) => {
|
|
132
132
|
e.preventDefault();
|
|
133
133
|
props.onClick?.(e);
|
|
134
134
|
}}/>)}>
|
|
@@ -153,7 +153,7 @@ describe('useRender', () => {
|
|
|
153
153
|
});
|
|
154
154
|
test('function render prop with children as function', async () => {
|
|
155
155
|
const Button = createBtn();
|
|
156
|
-
const { getByRole } = await render(<Button render={(
|
|
156
|
+
const { getByRole } = await render(<Button render={(props, state) => (<a {...props} href="/path" data-active={state.isActive} onClick={(e) => {
|
|
157
157
|
e.preventDefault();
|
|
158
158
|
props.onClick?.(e);
|
|
159
159
|
}}/>)}>
|
|
@@ -188,7 +188,7 @@ describe('useRender', () => {
|
|
|
188
188
|
const [isActive, setIsActive] = useState(false);
|
|
189
189
|
return useRender('button', { isActive }, {
|
|
190
190
|
props,
|
|
191
|
-
|
|
191
|
+
baseProps: {
|
|
192
192
|
onClick: (_e) => {
|
|
193
193
|
setIsActive((cur) => !cur);
|
|
194
194
|
return defaultHandler();
|