Skip to content

Commit

Permalink
fix(switch): support uncontrolled switch in react-hook-form (#2924)
Browse files Browse the repository at this point in the history
* feat(switch): add @nextui-org/use-safe-layout-effect

* chore(deps): add @nextui-org/use-safe-layout-effect

* fix(switch): react-hook-form uncontrolled switch component

* fix(switch): react-hook-form uncontrolled switch component

* feat(switch): add rect-hook-form in dev dep

* feat(switch): add WithReactHookFormTemplate
  • Loading branch information
wingkwong committed May 3, 2024
1 parent 3748abe commit 9acf3ea
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-zoos-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/switch": patch
---

Fixed react-hook-form uncontrolled switch component
4 changes: 3 additions & 1 deletion packages/components/switch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
"@react-aria/focus": "^3.16.2",
"@react-aria/interactions": "^3.21.1",
"@react-aria/switch": "^3.6.2",
Expand All @@ -56,7 +57,8 @@
"@nextui-org/shared-icons": "workspace:*",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react-dom": "^18.0.0",
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}
23 changes: 17 additions & 6 deletions packages/components/switch/src/use-switch.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type {ToggleVariantProps, ToggleSlots, SlotsToClasses} from "@nextui-org/theme";
import type {FocusableRef} from "@react-types/shared";
import type {AriaSwitchProps} from "@react-aria/switch";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";

import {ReactNode, Ref, useCallback, useId, useRef, useState} from "react";
import {mapPropsVariants} from "@nextui-org/system";
import {mergeRefs} from "@nextui-org/react-utils";
import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect";
import {useHover, usePress} from "@react-aria/interactions";
import {toggle} from "@nextui-org/theme";
import {chain, mergeProps} from "@react-aria/utils";
import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils";
import {useFocusableRef} from "@nextui-org/react-utils";
import {useSwitch as useReactAriaSwitch} from "@react-aria/switch";
import {useMemo} from "react";
import {useToggleState} from "@react-stately/toggle";
Expand All @@ -27,7 +27,7 @@ interface Props extends HTMLNextUIProps<"input"> {
/**
* Ref to the DOM node.
*/
ref?: Ref<HTMLElement>;
ref?: Ref<HTMLInputElement>;
/**
* The label of the switch.
*/
Expand Down Expand Up @@ -100,8 +100,9 @@ export function useSwitch(originalProps: UseSwitchProps = {}) {

const Component = as || "label";

const inputRef = useRef(null);
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
const domRef = useRef<HTMLLabelElement>(null);

const inputRef = useRef<HTMLInputElement>(null);

const labelId = useId();

Expand Down Expand Up @@ -139,6 +140,16 @@ export function useSwitch(originalProps: UseSwitchProps = {}) {

const state = useToggleState(ariaSwitchProps);

// if we use `react-hook-form`, it will set the switch value using the ref in register
// i.e. setting ref.current.checked to true or false which is uncontrolled
// hence, sync the state with `ref.current.checked`
useSafeLayoutEffect(() => {
if (!inputRef.current) return;
const isInputRefChecked = !!inputRef.current.checked;

state.setSelected(isInputRefChecked);
}, [inputRef.current]);

const {
inputProps,
isPressed: isPressedKeyboard,
Expand Down Expand Up @@ -212,7 +223,7 @@ export function useSwitch(originalProps: UseSwitchProps = {}) {
const getInputProps: PropGetter = (props = {}) => {
return {
...mergeProps(inputProps, focusProps, props),
ref: inputRef,
ref: mergeRefs(inputRef, ref),
id: inputProps.id,
onChange: chain(onChange, inputProps.onChange),
};
Expand Down
48 changes: 48 additions & 0 deletions packages/components/switch/stories/switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {toggle} from "@nextui-org/theme";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {SunFilledIcon, MoonFilledIcon} from "@nextui-org/shared-icons";
import {clsx} from "@nextui-org/shared-utils";
import {button} from "@nextui-org/theme";
import {useForm} from "react-hook-form";

import {Switch, SwitchProps, SwitchThumbIconProps, useSwitch} from "../src";

Expand Down Expand Up @@ -131,6 +133,44 @@ const CustomWithHooksTemplate = (args: SwitchProps) => {
);
};

const WithReactHookFormTemplate = (args: SwitchProps) => {
const {
register,
formState: {errors},
handleSubmit,
} = useForm({
defaultValues: {
defaultTrue: true,
defaultFalse: false,
requiredField: false,
},
});

const onSubmit = (data: any) => {
// eslint-disable-next-line no-console
console.log(data);
alert("Submitted value: " + JSON.stringify(data));
};

return (
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<Switch {...args} {...register("defaultTrue")}>
By default this switch is true
</Switch>
<Switch {...args} {...register("defaultFalse")}>
By default this switch is false
</Switch>
<Switch {...args} {...register("requiredField", {required: true})}>
This switch is required
</Switch>
{errors.requiredField && <span className="text-danger">This switch is required</span>}
<button className={button({class: "w-fit"})} type="submit">
Submit
</button>
</form>
);
};

export const Default = {
args: {
...defaultProps,
Expand Down Expand Up @@ -204,3 +244,11 @@ export const CustomWithHooks = {
...defaultProps,
},
};

export const WithReactHookForm = {
render: WithReactHookFormTemplate,

args: {
...defaultProps,
},
};
13 changes: 6 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9acf3ea

Please sign in to comment.