mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-24 01:09:48 +01:00
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:
committed by
GitHub
parent
58a8232aa9
commit
aa98044290
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 />)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 />)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user