UNPKG

@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
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; }, [{}, []]);