Form elements that support react-hook-form can now be made read-only (#1696)

The recently integrated form library react-hook-form does not submit disabled inputs, but a behaviour where interaction with an input is not possible and it is still submitted is necessary. This feature implements a readOnly property for all components that support react-hook-form. It is visually indistinguishable from a disabled input but will be submitted when the form is submitted. All form fields use disabled fieldset wrappers to accomplish this goal because react-hook-form only checks the disabled property on the input itself, not any ancestors, and the inputs are still correctly displayed as disabled.
This commit is contained in:
Konstantin Schaper
2021-06-15 10:11:59 +02:00
committed by GitHub
parent 58a8232aa9
commit aa98044290
12 changed files with 557 additions and 223 deletions

View File

@@ -56,10 +56,17 @@ const Ref: FC = () => {
type Settings = {
rememberMe: string;
scramblePassword: string;
readonly: boolean;
disabled: boolean;
};
const ReactHookForm: FC = () => {
const { register, handleSubmit } = useForm<Settings>();
const { register, handleSubmit } = useForm<Settings>({
defaultValues: {
disabled: true,
readonly: true
}
});
const [stored, setStored] = useState<Settings>();
const onSubmit = (settings: Settings) => {
@@ -71,6 +78,8 @@ const ReactHookForm: FC = () => {
<form onSubmit={handleSubmit(onSubmit)}>
<Checkbox label="Remember Me" {...register("rememberMe")} />
<Checkbox label="Scramble Password" {...register("scramblePassword")} />
<Checkbox label="Disabled wont be submitted" disabled={true} {...register("disabled")} />
<Checkbox label="Readonly will be submitted" readOnly={true} {...register("readonly")} />
<div className="pt-2">
<SubmitButton>Submit</SubmitButton>
</div>

View File

@@ -42,6 +42,7 @@ type BaseProps = {
helpText?: string;
testId?: string;
className?: string;
readOnly?: boolean;
};
const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
@@ -51,6 +52,7 @@ const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
disabled,
testId,
className,
readOnly,
...props
}) => {
const field = useInnerRef(props.innerRef);
@@ -95,7 +97,7 @@ const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
}
};
return (
<div className="field">
<fieldset className="field" disabled={readOnly}>
{renderLabelWithHelp()}
<div className="control">
{/*
@@ -113,13 +115,14 @@ const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
ref={field}
checked={props.checked}
disabled={disabled}
readOnly={readOnly}
{...createAttributesForTesting(testId)}
/>{" "}
{label}
{renderHelp()}
</label>
</div>
</div>
</fieldset>
);
};

View File

@@ -62,6 +62,8 @@ const AutoFocusAndRef: FC = () => {
};
type Name = {
readonly: string;
disabled: string;
firstName: string;
lastName: string;
};
@@ -70,7 +72,7 @@ const ReactHookForm: FC = () => {
const {
register,
handleSubmit,
formState: { errors }
formState: { errors },
} = useForm<Name>();
const [stored, setStored] = useState<Person>();
@@ -81,6 +83,18 @@ const ReactHookForm: FC = () => {
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<InputField
label="Readonly"
defaultValue="I am readonly but still show up on submit!"
readOnly={true}
{...register("readonly")}
/>
<InputField
label="Disabled"
defaultValue="I am disabled and dont show up on submit!"
disabled={true}
{...register("disabled")}
/>
<InputField label="First Name" autofocus={true} {...register("firstName")} />
<InputField
label="Last Name"
@@ -107,15 +121,15 @@ const LegacyEvents: FC = () => {
const [value, setValue] = useState<string>("");
return (
<>
<InputField placeholder="Legacy onChange handler" value={value} onChange={e => setValue(e)} />
<InputField placeholder="Legacy onChange handler" value={value} onChange={(e) => setValue(e)} />
<div className="mt-3">{value}</div>
</>
);
};
storiesOf("Forms|InputField", module)
.addDecorator(storyFn => <Decorator>{storyFn()}</Decorator>)
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
.addDecorator((storyFn) => <Decorator>{storyFn()}</Decorator>)
.addDecorator((storyFn) => <MemoryRouter>{storyFn()}</MemoryRouter>)
.add("AutoFocus", () => <InputField label="Field with AutoFocus" autofocus={true} />)
.add("Default Value", () => <InputField label="Field with Default Value" defaultValue={"I am a default value"} />)
.add("Ref", () => <Ref />)

View File

@@ -44,6 +44,7 @@ type BaseProps = {
className?: string;
testId?: string;
defaultValue?: string;
readOnly?: boolean;
};
export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>> = ({
@@ -62,6 +63,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
testId,
autofocus,
defaultValue,
readOnly,
...props
}) => {
const field = useAutofocus<HTMLInputElement>(autofocus, props.innerRef);
@@ -101,7 +103,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
helper = <p className="help is-info">{informationMessage}</p>;
}
return (
<div className={classNames("field", className)}>
<fieldset className={classNames("field", className)} disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<input
@@ -120,7 +122,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
/>
</div>
{helper}
</div>
</fieldset>
);
};

View File

@@ -65,6 +65,8 @@ const Ref: FC = () => {
type Settings = {
rememberMe: string;
scramblePassword: string;
readonly: string;
disabled: string;
};
const ReactHookForm: FC = () => {
@@ -83,6 +85,8 @@ const ReactHookForm: FC = () => {
<Radio value={"false"} label="Dont Remember Me" {...register("rememberMe")} />
</RadioList>
<Radio className="ml-2" value={"false"} label="Scramble Password" {...register("scramblePassword")} />
<Radio value={"false"} label="Disabled wont be submitted" disabled={true} {...register("disabled")} />
<Radio value={"false"} label="Readonly will be submitted" {...register("readonly")} />
<div className="pt-2">
<SubmitButton>Submit</SubmitButton>
</div>

View File

@@ -31,6 +31,10 @@ const StyledRadio = styled.label`
margin-right: 0.5em;
`;
const InlineFieldset = styled.fieldset`
display: inline-block;
`;
type BaseProps = {
label?: string;
name?: string;
@@ -40,9 +44,10 @@ type BaseProps = {
helpText?: string;
defaultChecked?: boolean;
className?: string;
readOnly?: boolean;
};
const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({ name, defaultChecked, ...props }) => {
const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({ name, defaultChecked, readOnly, ...props }) => {
const renderHelp = () => {
const helpText = props.helpText;
if (helpText) {
@@ -71,7 +76,7 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({ name
};
return (
<>
<InlineFieldset disabled={readOnly}>
{/*
we have to ignore the next line,
because jsx label does not the custom disabled attribute
@@ -92,7 +97,7 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({ name
{props.label}
{renderHelp()}
</StyledRadio>
</>
</InlineFieldset>
);
};

View File

@@ -62,6 +62,8 @@ const Ref: FC = () => {
type Settings = {
rememberMe: string;
scramblePassword: string;
disabled: string;
readonly: string;
};
const ReactHookForm: FC = () => {
@@ -91,6 +93,26 @@ const ReactHookForm: FC = () => {
label="Scramble Password"
{...register("scramblePassword")}
/>
<Select
options={[
{ label: "Yes", value: "true" },
{ label: "No", value: "false" }
]}
label="Disabled wont be submitted"
defaultValue="false"
disabled={true}
{...register("disabled")}
/>
<Select
options={[
{ label: "Yes", value: "true" },
{ label: "No", value: "false" }
]}
label="Readonly will be submitted"
readOnly={true}
defaultValue="false"
{...register("readonly")}
/>
<div className="pt-2">
<SubmitButton>Submit</SubmitButton>

View File

@@ -43,6 +43,7 @@ type BaseProps = {
disabled?: boolean;
testId?: string;
defaultValue?: string;
readOnly?: boolean;
};
const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
@@ -54,6 +55,7 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
loading,
disabled,
testId,
readOnly,
...props
}) => {
const field = useInnerRef(props.innerRef);
@@ -96,7 +98,7 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
const loadingClass = loading ? "is-loading" : "";
return (
<div className="field">
<fieldset className="field" disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className={classNames("control select", loadingClass)}>
<select
@@ -118,7 +120,7 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
})}
</select>
</div>
</div>
</fieldset>
);
};

View File

@@ -29,6 +29,7 @@ import Button from "../buttons/Button";
import { useForm } from "react-hook-form";
import { SubmitButton } from "../buttons";
import { MemoryRouter } from "react-router-dom";
import InputField from "./InputField";
const Spacing = styled.div`
padding: 2em;
@@ -38,7 +39,7 @@ const OnChangeTextarea = () => {
const [value, setValue] = useState("Start typing");
return (
<Spacing>
<Textarea value={value} onChange={v => setValue(v)} />
<Textarea value={value} onChange={(v) => setValue(v)} />
<hr />
<p>{value}</p>
</Spacing>
@@ -56,7 +57,7 @@ const OnSubmitTextare = () => {
return (
<Spacing>
<Textarea value={value} onChange={v => setValue(v)} onSubmit={submit} />
<Textarea value={value} onChange={(v) => setValue(v)} onSubmit={submit} />
<hr />
<p>{submitted}</p>
</Spacing>
@@ -72,7 +73,7 @@ const OnCancelTextarea = () => {
return (
<Spacing>
<Textarea value={value} onChange={v => setValue(v)} onCancel={cancel} />
<Textarea value={value} onChange={(v) => setValue(v)} onCancel={cancel} />
</Spacing>
);
};
@@ -105,13 +106,15 @@ const AutoFocusAndRef: FC = () => {
type Commit = {
message: string;
footer: string;
readonly: string;
disabled: string;
};
const ReactHookForm: FC = () => {
const {
register,
handleSubmit,
formState: { errors }
formState: { errors },
} = useForm<Commit>();
const [stored, setStored] = useState<Commit>();
@@ -130,6 +133,18 @@ const ReactHookForm: FC = () => {
errorMessage={"Message is required"}
/>
<Textarea label="Footer" {...register("footer")} />
<Textarea
label="Readonly"
readOnly={true}
defaultValue="I am readonly but still show up on submit!"
{...register("readonly")}
/>
<Textarea
label="Disabled"
disabled={true}
defaultValue="I am disabled and dont show up on submit!"
{...register("disabled")}
/>
<div className="pt-2">
<SubmitButton>Submit</SubmitButton>
</div>
@@ -149,14 +164,14 @@ const LegacyEvents: FC = () => {
const [value, setValue] = useState<string>("");
return (
<>
<Textarea placeholder="Legacy onChange handler" value={value} onChange={e => setValue(e)} />
<Textarea placeholder="Legacy onChange handler" value={value} onChange={(e) => setValue(e)} />
<div className="mt-3">{value}</div>
</>
);
};
storiesOf("Forms|Textarea", module)
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
.addDecorator((storyFn) => <MemoryRouter>{storyFn()}</MemoryRouter>)
.add("OnChange", () => <OnChangeTextarea />)
.add("OnSubmit", () => <OnSubmitTextare />)
.add("OnCancel", () => <OnCancelTextarea />)

View File

@@ -41,6 +41,7 @@ type BaseProps = {
errorMessage?: string | string[];
informationMessage?: string;
defaultValue?: string;
readOnly?: boolean;
};
const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
@@ -57,6 +58,7 @@ const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
validationError,
informationMessage,
defaultValue,
readOnly,
...props
}) => {
const ref = useAutofocus<HTMLTextAreaElement>(autofocus, props.innerRef);
@@ -101,7 +103,7 @@ const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
}
return (
<div className="field">
<fieldset className="field" disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<textarea
@@ -118,7 +120,7 @@ const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
/>
</div>
{helper}
</div>
</fieldset>
);
};