Skip to content

v1.0.2

Compare
Choose a tag to compare
@edmundhung edmundhung released this 19 Feb 22:45
· 79 commits to main since this release

New APIs: unstable_useControl and <unstable_Control />

In v1, the useInputControl hook is introduced with ability to insert an hidden input for you. Unfortunately, this is found to be problematic in situations where you need to dynamically render the input. After some discussions, we believe it would be better stop supporting this and have developers deciding how they wanna render the hidden input instead. To avoid breaking changes on the useInputControl hook, a new useControl hook is introduced which works similar to useInputControl() except you are required to register the input element yourself:

Here is an example with Headless UI Listbox

function Select({
    name,
    options,
    placeholder,
}: {
    name: FieldName<string>;
    placeholder: string;
    options: string[];
}) {
    const [field] = useField(name);
    const control = useControl(field);

    return (
        <Listbox
            value={control.value ?? ''}
            onChange={value => control.change(value)}
        >
            {/* Render a select element manually and register with a callback ref */}
            <select
                className="sr-only"
                aria-hidden
                tabIndex={-1}
                ref={control.register}
                name={field.name}
                defaultValue={field.initialValue}
            >
                <option value="" />
                {options.map((option) => (
                    <option key={option} value={option} />
                ))}
            </select>
            <div className="relative mt-1">
                <Listbox.Button className="...">
                    <span className="block truncate">
                        {control.value ?? placeholder}
                    </span>
                    <span className="...">
                        <ChevronUpDownIcon
                            className="h-5 w-5 text-gray-400"
                            aria-hidden="true"
                        />
                    </span>
                </Listbox.Button>
                <Listbox.Options className="...">
                    {options.map((option) => (
                        <Listbox.Option
                            key={option}
                            className="..."
                            value={option}
                        >
                            {({ selected, active }) => (
                                <>
                                    <span className="...">
                                        {option}
                                    </span>

                                    {option !== '' && selected ? (
                                        <span className="...">
                                            <CheckIcon className="h-5 w-5" aria-hidden="true" />
                                        </span>
                                    ) : null}
                                </>
                            )}
                        </Listbox.Option>
                    ))}
                </Listbox.Options>
            </div>
        </Listbox>
    );
}

You might also find the <Control /> component in the render-props style useful when working with checkbox group in which we can use it to register each individual checkbox element without creating an additional component. Here is an example based on Radix Checkbox:

function CheckboxGroup({
    name,
    options,
}: {
    name: FieldName<string[]>;
    options: string[];
}) {
    const [field] = useField(name);
    // The initialValue can be a string or string array depending on how it was submitted before. 
    // To make it easy working with the initial value, we make sure it is always an array
    const initialValue =
        typeof field.initialValue === 'string'
            ? [field.initialValue]
            : field.initialValue ?? [];

    return (
        <div className="py-2 space-y-4">
            {options.map((option) => (
                <Control
                    key={option}
                    meta={{
                        key: field.key,
                        initialValue: initialValue.includes(option) ? option : '',
                    }}
                    render={(control) => (
                        <div
                            className="flex items-center"
                            ref={(element) => {
                                // Radix does not expose the inner input ref. That's why we query it from the container element
                                control.register(element?.querySelector('input'))
                            }}
                        >
                            <RadixCheckbox.Root
                                type="button"
                                className="flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-[4px] bg-white outline-none shadow-[0_0_0_2px_black]"
                                id={`${field.id}-${option}`}
                                name={field.name}
                                value={option}
                                checked={control.value === option}
                                onCheckedChange={(state) =>
                                    control.change(state.valueOf() ? option : '')
                                }
                                onBlur={control.blur}
                            >
                                <RadixCheckbox.Indicator>
                                    <CheckIcon className="w-4 h-4" />
                                </RadixCheckbox.Indicator>
                            </RadixCheckbox.Root>
                            <label
                                htmlFor={`${field.id}-${option}`}
                                className="pl-[15px] text-[15px] leading-none"
                            >
                                {option}
                            </label>
                        </div>
                    )}
                />
            ))}
        </div>
    );
}

Feel free to open a discussion if you have any issue using the new APIs. We will deprecate useInputControl and remove the unstable prefix once we are confident with the new APIs.

Improvements

  • Improved type inference with nested discriminated union (#459)
  • Fixed an issue with zod not coercing the value of a multi select correctly (#447)
  • Fixed an issue with react devtool inspecting the state of the useForm hook.
  • Fixed an issue with file input never marked as dirty (#457)
  • Fixed several typos on the docs. Thanks to @ngbrown (#449) and @Kota-Yamaguchi (#463)

New Contributors

Full Changelog: v1.0.1...v1.0.2