@solid-primitives/controlled-props
Version:
The primitives in this package allow you to create controlls for component props.
114 lines (113 loc) • 4.45 kB
JSX
import { createMemo, createSignal, For } from "solid-js";
export const BoolProp = props => (<label>
<input type="checkbox" name={props.name} checked={props.value()} onChange={ev => props.setValue(ev.currentTarget.checked)}/>{" "}
<span>{props.name}</span>{" "}
</label>);
export const NumberProp = props => (<label>
<span>{props.name}</span>{" "}
<input type="number" name={props.name} min={props.min} max={props.max} value={props.value()} onChange={ev => props.setValue(ev.currentTarget.valueAsNumber)}/>{" "}
</label>);
export const StringProp = props => (<label>
<span>{props.name}</span>{" "}
<input type="text" name={props.name} value={props.value()} onInput={ev => props.setValue(ev.currentTarget.value)} onChange={ev => props.setValue(ev.currentTarget.value)}/>{" "}
</label>);
const filterEnum = (options) => {
const entries = Object.entries(options);
if (Object.keys(options).every(key => key === options[options[key]].toString())) {
return entries
.reduce((items, [key, value]) => {
if (!items.includes(key)) {
items.push(value);
}
return items;
}, [])
.map(item => [item, options[item]]);
}
return entries;
};
export const SelectProp = (props) => {
const options = createMemo(() => Array.isArray(props.options)
? props.options.map((option) => [option, option])
: filterEnum(props.options));
const initialValue = options().findIndex(([, value]) => value === props.value());
return (<label>
<span>{props.name}</span>{" "}
<select name={props.name} value={initialValue.toString()} onChange={ev => props.setValue(options()[ev.currentTarget.selectedIndex][1])}>
<For each={options()} fallback={<option>"options missing"</option>}>
{([key], index) => <option value={index()}>{key}</option>}
</For>
</select>{" "}
</label>);
};
const defaultInitialValues = {
boolean: false,
number: 0,
string: "",
object: undefined,
};
export function createControlledProp(name, options) {
const initialValue = options == null
? undefined
: typeof options !== "object"
? options
: (options.initialValue ??
defaultInitialValues[options.type ?? "object"]);
if (initialValue == null) {
throw new Error(`cannot get type for Prop ${name}`);
}
const propType = options.options
? "object"
: (options.type ?? typeof initialValue);
const [value, setValue] = createSignal(initialValue, { name });
return [
value,
setValue,
propType === "boolean"
? () => BoolProp({ name, value: value, setValue: setValue })
: propType === "number"
? () => NumberProp({ name, value: value, setValue: setValue })
: propType === "string"
? () => StringProp({ name, value: value, setValue: setValue })
: () => SelectProp({
name,
value: value,
setValue: setValue,
options: options.options ??
[initialValue],
}),
];
}
/**
* creates reactive props for testing a component
*
* @param props {Record<string, TestPropOptions>}
* @returns ```ts
* [
* props: { [name: string]: Accessor<T>, [setName: string]: Setter<T> }
* fields: JSX.Element[]
* ]
* ```
* You get the fields to render the prop controls and getter and setter names derived from the name in `props` based on common practice, i.e. `count` would automatically translate to
* ```ts
* { count: Accessor<T>, getCount: Setter<T> }
* ```
* @example ```ts
* const [props, fields] = createControlledProps({
* value: { initialValue: '' },
* disabled: { initialValue: false },
* invalid: { initialValue: false },
* type: { initialValue: 'text', options: ['text', 'password', 'email'] }
* })
* return <>
* <Input {...props} />
* {fields}
* </>
* ```
*/
export const createControlledProps = props => Object.entries(props).reduce((result, [name, options]) => {
const [value, setValue, field] = createControlledProp(name, options);
result[0][name] = value;
result[0][`set${name.slice(0, 1).toUpperCase()}${name.slice(1)}`] = setValue;
result[1].push(field({}));
return result;
}, [{}, []]);