Skip to content

Commit 0111a2f

Browse files
RusalnShchepotin
authored andcommitted
feat: add FormSwitchInput component with RHF integration and Storybook
1 parent 07fce3c commit 0111a2f

File tree

10 files changed

+284
-0
lines changed

10 files changed

+284
-0
lines changed

.idea/.gitignore

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/codeStyles/Project.xml

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/codeStyles/codeStyleConfig.xml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/extensive-react-boilerplate.iml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/prettier.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react";
2+
import { Meta, StoryFn } from "@storybook/react";
3+
import { useForm, FormProvider } from "react-hook-form";
4+
import FormSwitchInput, { SwitchInputProps } from "./form-switch";
5+
6+
interface Option {
7+
id: number;
8+
name: string;
9+
}
10+
11+
export default {
12+
title: "Components/Form/SwitchInput",
13+
component: FormSwitchInput,
14+
} as Meta;
15+
16+
const Template: StoryFn<SwitchInputProps<Option> & { name: string }> = (
17+
args
18+
) => {
19+
const methods = useForm({
20+
defaultValues: {
21+
switchField: [],
22+
},
23+
});
24+
25+
return (
26+
<FormProvider {...methods}>
27+
<form>
28+
<FormSwitchInput {...args} />
29+
</form>
30+
</FormProvider>
31+
);
32+
};
33+
34+
export const Default = Template.bind({});
35+
Default.args = {
36+
label: "Toggle options",
37+
name: "switchField",
38+
options: [
39+
{ id: 1, name: "Option A" },
40+
{ id: 2, name: "Option B" },
41+
{ id: 3, name: "Option C" },
42+
],
43+
keyValue: "id",
44+
keyExtractor: (option) => option.id.toString(),
45+
renderOption: (option) => option.name,
46+
testId: "switch-input",
47+
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client";
2+
3+
import {
4+
Controller,
5+
ControllerProps,
6+
FieldPath,
7+
FieldValues,
8+
} from "react-hook-form";
9+
10+
import FormControl from "@mui/material/FormControl";
11+
import FormHelperText from "@mui/material/FormHelperText";
12+
import FormLabel from "@mui/material/FormLabel";
13+
import FormControlLabel from "@mui/material/FormControlLabel";
14+
import FormGroup from "@mui/material/FormGroup";
15+
import Switch from "@mui/material/Switch";
16+
import { ForwardedRef, forwardRef } from "react";
17+
18+
export type SwitchInputProps<T> = {
19+
label: string;
20+
autoFocus?: boolean;
21+
disabled?: boolean;
22+
readOnly?: boolean;
23+
error?: string;
24+
testId?: string;
25+
keyValue: keyof T;
26+
options: T[];
27+
keyExtractor: (option: T) => string;
28+
renderOption: (option: T) => React.ReactNode;
29+
};
30+
31+
function SwitchInputRaw<T>(
32+
props: SwitchInputProps<T> & {
33+
name: string;
34+
value: T[] | undefined | null;
35+
onChange: (value: T[]) => void;
36+
onBlur: () => void;
37+
},
38+
ref?: ForwardedRef<HTMLDivElement | null>
39+
) {
40+
const value = props.value ?? [];
41+
42+
const onChange = (switchValue: T) => () => {
43+
const isExist = value
44+
.map((option) => option[props.keyValue])
45+
.includes(switchValue[props.keyValue]);
46+
47+
const newValue = isExist
48+
? value.filter(
49+
(option) => option[props.keyValue] !== switchValue[props.keyValue]
50+
)
51+
: [...value, switchValue];
52+
53+
props.onChange(newValue);
54+
};
55+
56+
return (
57+
<FormControl
58+
component="fieldset"
59+
variant="standard"
60+
error={!!props.error}
61+
data-testid={props.testId}
62+
>
63+
<FormLabel component="legend" data-testid={`${props.testId}-label`}>
64+
{props.label}
65+
</FormLabel>
66+
<FormGroup ref={ref}>
67+
{props.options.map((option) => (
68+
<FormControlLabel
69+
key={props.keyExtractor(option)}
70+
control={
71+
<Switch
72+
checked={value
73+
.map((val) => val[props.keyValue])
74+
.includes(option[props.keyValue])}
75+
onChange={onChange(option)}
76+
name={props.name}
77+
data-testid={`${props.testId}-${props.keyExtractor(option)}`}
78+
/>
79+
}
80+
label={props.renderOption(option)}
81+
/>
82+
))}
83+
</FormGroup>
84+
{!!props.error && (
85+
<FormHelperText data-testid={`${props.testId}-error`}>
86+
{props.error}
87+
</FormHelperText>
88+
)}
89+
</FormControl>
90+
);
91+
}
92+
93+
const SwitchInput = forwardRef(SwitchInputRaw) as never as <T>(
94+
props: SwitchInputProps<T> & {
95+
name: string;
96+
value: T[] | undefined | null;
97+
onChange: (value: T[]) => void;
98+
onBlur: () => void;
99+
} & { ref?: ForwardedRef<HTMLDivElement | null> }
100+
) => ReturnType<typeof SwitchInputRaw>;
101+
102+
function FormSwitchInput<
103+
TFieldValues extends FieldValues = FieldValues,
104+
T = unknown,
105+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
106+
>(
107+
props: SwitchInputProps<T> &
108+
Pick<ControllerProps<TFieldValues, TName>, "name" | "defaultValue">
109+
) {
110+
return (
111+
<Controller
112+
name={props.name}
113+
defaultValue={props.defaultValue}
114+
render={({ field, fieldState }) => (
115+
<SwitchInput<T>
116+
{...field}
117+
label={props.label}
118+
error={fieldState.error?.message}
119+
disabled={props.disabled}
120+
readOnly={props.readOnly}
121+
testId={props.testId}
122+
options={props.options}
123+
keyValue={props.keyValue}
124+
keyExtractor={props.keyExtractor}
125+
renderOption={props.renderOption}
126+
/>
127+
)}
128+
/>
129+
);
130+
}
131+
132+
export default FormSwitchInput;

0 commit comments

Comments
 (0)