Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 1x 1x 1x 1x 13x 2x 2x 13x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 9x 9x 1x 1x 1x 9x 1x 1x 1x 9x 1x 9x 1x 5x 2x 2x 2x 2x 9x 9x 16x 16x 16x 3x 3x 3x 3x 3x 3x 3x 3x 16x 13x 13x 13x 13x 13x 13x 13x 13x 13x 16x 13x 13x 13x | import { type ChangeEvent, type FunctionComponent, useCallback } from 'react';
import { type Bindable, getRefState, isRef, resolveProps, useObserverRef, useValue } from '../index.js';
import type { BindingType, InitProps, InputBinding, InputBindingProps } from './Types.js';
const CONVERTIBLE = new Set<BindingType | undefined>(['number', 'range', 'date']);
/**
* A higher-order component (HOC) that wraps a given component to enable two-way data binding.
*
* This HOC provides automatic synchronization between the component's input value and a bindable state.
* It handles various input types including text, number, range, date, checkbox, and radio inputs.
*
* @template Props - The props type of the wrapped component
* @param Component - The component to be wrapped with binding functionality
* @param displayName - Optional display name for the resulting component
* @returns A new component with binding capabilities
*/
export function bindable<Props extends InitProps>(Component: FunctionComponent<Props>, displayName?: string) {
if (displayName && !Component.displayName) {
Component.displayName = displayName;
}
/**
* The binding component that wraps the original component.
*
* @template Bind - The bindable state type
* @template Kind - The binding type (text, number, date, etc.)
*/
function Binding<Bind extends Bindable, Kind extends BindingType = 'text'>(
props: InputBindingProps<Kind, Bind, Props>
) {
const [observer] = useObserverRef();
const { type, bind, bindKey, name, onChange, value, checked, ...allProps } = props as Props;
const restProps = observer.run(() => resolveProps(allProps as Props));
const key = isRef(bind) ? 'value' : (bindKey ?? name);
const val = type === 'checkbox' || type === 'radio' ? (checked ?? false) : (value ?? '');
const current = useValue(getRefState(bind), key) ?? val;
const handleInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (!bind) return onChange?.(e);
if (type !== 'checkbox' && type !== 'radio' && (e.target.value === null || e.target.value === undefined)) {
delete bind[key];
return onChange?.(e);
}
if (CONVERTIBLE.has(type) && !e.target.value) {
delete bind[key];
return onChange?.(e);
}
if (type === 'number' || type === 'range') {
bind[key] = parseFloat(e.target.value);
} else if (type === 'date') {
bind[key] = new Date(e.target.value);
} else if (type === 'checkbox' || type === 'radio') {
bind[key] = e.target.checked ?? false;
} else {
bind[key] = e.target.value;
}
return onChange?.(e);
},
[bind, key]
);
if (type === 'checkbox' || type === 'radio') {
return (
<Component
type={type}
name={name}
checked={current}
onChange={handleInputChange}
{...(restProps as never as Props)}
/>
);
} else {
return (
<Component
type={type}
name={name}
value={current}
onChange={handleInputChange}
{...(restProps as never as Props)}
/>
);
}
}
Binding.displayName = `Binding(${displayName || Component.displayName || 'Anonymous'})`;
return Binding as never as InputBinding<Props>;
}
|