What is the recommended way to reset a form with custom form inputs? #8400
-
Let's say we have the following form: export async function loader({ request }: LoaderFunctionArgs) {
return json({ title: 'My awesome Draft PR', description: "Great PR description..." })
}
export async function action({ request }: ActionFunctionArgs) {
// ...
}
export function EditPullRequest() {
const { title, reviewers } = useLoaderData<typeof loader>();
return (
<Form method="POST">
<input name="title" defaultValue={ title } />
<RichTextEditor name="reviewers" defaultValue={ description } />
<button type="reset">Discard</button>
<button type="submit">Update</button>
</Form>
)
}
function RichTextEditor({ name, defaultValue }) {
const [value, setValue] = useState(defaultValue)
return (
<>
<SomeEditor value={ value } onChange={ setValue } />
{/* Hidden textarea that mirrors state of editor for form submission */}
<textarea name={ name } className="hidden" defaultValue={ value } value={ value } />
</>
)
} We know that input and textarea are both resettable elements so clicking on the Discard button will automatically reset the Unfortunately resetting a form does NOT trigger any events in the form's children by design - unfortunately we can't do something like: Ideally I do not want to lift the RichTextEditor state into the parent - I'm looking for a pattern that will be contained inside my custom form elements (i.e. RichTextEditor) such that the developer doesn't need to worry about wiring the form's onReset event with resetting all the controlled components. For the keen eyed among you 👀You may have noticed that having both a function RichTextEditor({ name, defaultValue }) {
// ...
useEffect(() => {
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
)?.set
nativeTextAreaValueSetter?.call(ref.current, value)
// optionally dispatch `change` event useful for `form.onChange()` to work properly
}, [value])
return (
<>
<SomeEditor value={ value } onChange={ setValue } />
<textarea name={ name } type="hidden" defaultValue={ value } />
</>
)
} We almost need some 2-way data binding between the hidden textarea and the |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
Your custom input could find the closest https://developer.mozilla.org/en-US/docs/Web/API/Element/closest https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset_event |
Beta Was this translation helpful? Give feedback.
-
I think I can answer my own question here but will still open to hear any other solutions from the community. My goal in all of this is to create custom form elements that will behave just like native form elements (ie input, textarea, select) where the developer can put it inside a Form submissionsFor this I think the path is pretty clear: use hidden inputs: function MyCustomFormElement = () => {
const [value, setValue] = useState('')
return (
<>
<SomeStatefulFormComponent value={value} onChange={setOnChange} />
{/* tip: can use <textarea /> instead if you need to preserve new lines*/}
<input type="hidden" value={value} />
</>
)
} Form.onChangeI want react's Form.onChange event to work because in my UX I want to show if there are "unsaved changes" in the form. <Form onChange={(e) => setIsDirty(isFormDirty(e.currentTarget))} Unfortunately we cannot update the hidden inputs value AND emit a "change" event. The change event will be swallowed due to react event deduping. To get around this we can use the value setter on the prototype: function MyCustomFormElement = () => {
const [value, setValue] = useState('')
const hiddenInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!hiddenInputRef.current) return // if not inside form, ignore
const inputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value',
)?.set
inputValueSetter?.call(hiddenInputRef.current, value)
hiddenInputRef.current.dispatchEvent(new Event('change', { bubbles: true }))
}, [value])
return (
<>
<SomeStatefulFormComponent value={value} onChange={setOnChange} />
<input ref={hiddenInputRef} type="hidden" defaultValue={value} />
</>
)
} To read more about this solution see: facebook/react#10135 (comment) Form.onResetWe want our custom form elements to reset to defaultValue whenever From.reset() is triggered (ie user clicks on function MyCustomFormElement = ({ defaultValue }) => {
const [value, setValue] = useState('')
const hiddenInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// this gets the form element with which the input is associated with
// see: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form
const form = hiddenInputRef.current?.form
if (!form) return
const resetHandler = event => {
if(event.target === form) {
setValue(defaultValue)
}
}
document.addEventListener('reset', resetHandler)
return () => document.removeEventListener('reset', resetHandler)
}, [defaultValue])
// rest of component... (note: I haven't tested this code) TLDRTurns out this has all been thoughtfully built by at least these 2 libraries:
Hope this helps someone out there. |
Beta Was this translation helpful? Give feedback.
I think I can answer my own question here but will still open to hear any other solutions from the community.
My goal in all of this is to create custom form elements that will behave just like native form elements (ie input, textarea, select) where the developer can put it inside a
<Form>
and expect Form submissions, Form.onChange and Form.onReset to just work™️ .Form submissions
For this I think the path is pretty clear: use hidden inputs: