Unable to focus form element on modal open with react-hook-form - reactjs

I'm trying to solve a problem that seems quite straight forward. I want to focus a form element as soon as a modal opens.
Since react-hook-form does not provide a direct handle to the form refs, I'm trying to use a useEffect hook and react-hook-forms howngrown setFocus functions but I keep seeing the error:
s.focus is not a function
Code
--> Link to coding sandbox
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
import {
ChakraProvider,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
Button,
ModalCloseButton,
useDisclosure
} from "#chakra-ui/react";
export default function App() {
const { setFocus, register } = useForm();
const { isOpen, onClose, onOpen } = useDisclosure();
useEffect(() => {
if (!isOpen) return;
setFocus("name");
}, [setFocus, isOpen]);
return (
<ChakraProvider>
<div className="App">
<Button onClick={onOpen}>Open</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
<input {...register("name")} />
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</ChakraProvider>
);
}

Maybe setFocus can't run because the modal content (and the input inside) is first unmounted.
An easy solution using setFocus is to make the form as an independant component :
const Form = () => {
const { setFocus, register } = useForm();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return <input {...register("name")} />;
};
and then include it inside the modalBody.
Fixed sandbox link

Read the Chakra documentation there is one prop initialFocusRef, you can pass the ref to the element you want to focus on initially
Chakra provides 2 props for this use case:
initialFocusRef: The ref of the component that receives focus when the modal opens.
finalFocusRef: The ref of the component that receives focus when the modal closes.
import { useEffect, useRef } from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
import {
ChakraProvider,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
Button,
ModalCloseButton,
useDisclosure
} from "#chakra-ui/react";
export default function App() {
const { setFocus, register } = useForm();
const { isOpen, onClose, onOpen } = useDisclosure();
const initialRef = useRef();
useEffect(() => {
//setFocus("name");
}, [setFocus, isOpen]);
return (
<ChakraProvider>
<div className="App">
<Button onClick={onOpen}>Open</Button>
<Modal isOpen={isOpen} onClose={onClose} initialFocusRef={initialRef}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
<input {...register("name")} ref={initialRef} />
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</ChakraProvider>
);
}
UPDATED BASED ON COMMENT
By default, Chkra focus enabled elements if you just add tabIndex to your elements and give order value appropriately then it will get focused. Previously close button was focused by default, now in the below code you will notice I have given tabIndex=2 to the close button and tabIndex=1 to the input element and it is working as expected.
<ChakraProvider>
<div className="App">
<Button onClick={onOpen}>Open</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton tabIndex="2" />
<ModalBody>
<input tabIndex="1" {...register("name")} />
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</ChakraProvider>
If you want to use that using ref then you can check "How to share ref usage?", maybe it will help you.

Related

Chakra ui modal not working on button click

I am trying to use chakra ui modal in my app. but this modal is not showing , when I directly pass true to to isOpen .its showing .but when i pass isOpen Disclosure it is not working. when I check the react dev tools. the value of isOpen is changed to true on the button click. but the popup is not coming
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
useDisclosure,
} from '#chakra-ui/react'
function Home() {
const {isOpen,onOpen,onClose} = useDisclosure()
return (
<>
<Button onClick={onOpen}>Open Modal</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
{/* <Lorem count={2} /> */}
</ModalBody>
<ModalFooter>
<Button colorScheme='blue' mr={3} onClick={onClose}>
Close
</Button>
<Button variant='ghost'>Secondary Action</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
export default Home

React Modal Submit Form

I'm trying to print out a simple buttton clicked text whenever the submit button is click on my reactstrap modal and somehow my code doesn't do anything, not sure what I have wrong.
I have attached a picture for better visualisation , I'm using reactstrap Modal.
import React, { useState } from "react";
import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
import Button from "./Button";
// NOTICE
// Modal is brought in with it's own trigger, so import the component where you want the trigger to be.
const ModalComponent = (props) => {
const {
buttonText,
title,
actionButtonText,
cancelButtonText,
children,
className,
} = props;
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
const alertshow = () => {
alert("button clicked");
};
const closeBtn = (
<button className="close" onClick={toggle}>
×
</button>
);
return (
<div>
<a className="btn_link" onClick={toggle}>
{buttonText}
</a>
<form onSubmit={alertshow}>
<Modal isOpen={modal} toggle={toggle} className={className}>
<ModalHeader className=" border-0" toggle={toggle} close={closeBtn}>
{title}
</ModalHeader>
<ModalBody className="text-left border-0">
<p className="modal-label">Please enter your email address</p>
{children}
</ModalBody>
<ModalFooter className="modal-footer border-0">
<Button className="btn_secondary modal-btn" onClick={toggle}>
{cancelButtonText}
</Button>{" "}
<input
className="btn btn_primary modal-btn"
type="submit"
value={actionButtonText}
/>
</ModalFooter>
</Modal>
</form>
</div>
);
};
export default ModalComponent;
Its happening form should be part of modal not modal should be part of form. This is why its not referencing onSubmit. You need to do this:
<Modal isOpen={modal} toggle={toggle} className={className}>
<form onSubmit={alertshow}>
...rest all content
</Form>
</Modal>
Here is full code:
import React, { useState } from "react";
import "./styles.css";
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from "reactstrap";
// NOTICE
// Modal is brought in with it's own trigger, so import the component where you want the trigger to be.
const ModalComponent = (props) => {
const {
buttonText,
title,
actionButtonText,
cancelButtonText,
children,
className
} = props;
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
const alertshow = () => {
alert("button clicked");
};
const closeBtn = (
<button className="close" onClick={toggle}>
×
</button>
);
return (
<div>
<div onClick={toggle}>{buttonText}</div>
<Modal isOpen={modal} toggle={toggle} className={className}>
<form onSubmit={alertshow}>
<ModalHeader className=" border-0" toggle={toggle} close={closeBtn}>
{title}
</ModalHeader>
<ModalBody className="text-left border-0">
<p className="modal-label">Please enter your email address</p>
{children}
</ModalBody>
<ModalFooter className="modal-footer border-0">
<Button className="btn_secondary modal-btn" onClick={toggle}>
{cancelButtonText}
</Button>{" "}
<input
className="btn btn_primary modal-btn"
type="submit"
value={actionButtonText}
/>
</ModalFooter>
</form>
</Modal>
</div>
);
};
export default function App() {
return (
<div className="App">
<ModalComponent
title="Hello"
cancelButtonText="Cancel"
actionButtonText="Submit"
buttonText="testing"
/>
</div>
);
}
Here is the demo: https://codesandbox.io/s/fervent-bash-51lxe?file=/src/App.js:0-1826
Accepted answer doesn't work when Modal is scrollable.
Here is how to resolve the issue:
<Modal show={ show } onHide={ onClose }
scrollable={ true }
onSubmit={ handleSubmit(onSave) }
dialogAs={ FormWrappedModal }>
<Modal.Header closeButton>
<Modal.Title>some title</Modal.Title>
</Modal.Header>
<Modal.Body>some body</Modal.Body>
<Modal.Footer>some body</Modal.Footer>
</Modal>
We need to introduce custom component FormWrappedModal for that purpose:
const FormWrappedModal = ( props: any)=>{
return (
<form onSubmit={ props.onSubmit }>
<Modal.Dialog { ...props } />
</form>
);
};

Cannot edit text inside a React Modal with State to Edit Task Title and Body

I'm wondering if anyone else has had success editing text inside a Bootstrap React Modal and could share some wisdom. I'm using a Bootstrap React Modal to display data once the user clicks on the task title. The data is displaying beautifully, but the user cannot edit the data. The cursor changes to an editing cursor, but no amount of keyboard pounding gets an edit. I tried adding a Bootstrap React Form as you can see below. When I click on the "Save Changes" button, I get my toastify error.
import React, { useState } from "react";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import Modal from "react-bootstrap/Modal";
import taskAPI from "../../utils/taskAPI";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import "./style.css";
function TaskModal({ onHide, onTaskUpdated, task }) {
const [title, setTitle] = useState("");
const [textBody, setTextBody] = useState("");
const [isPending, setIsPending] = useState(false);
const notify = () => toast.warn("You cannot save an empty task!");
const updateTask=(event)=> {
event.preventDefault();
setIsPending(true);
taskAPI
.updateTask({ title, textBody })
.then((response) => {
response.setTitle("");
response.setTextBody("");
})
.then((response) => {
onTaskUpdated();
setIsPending(false);
})
.catch((error) => {
console.log(error);
setIsPending(false);
notify();
});
}
return (
<>
<Modal show={true} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title>
<Form>
<Form.Group controlId="TaskTitleText">
<Form.Control
className="TaskTitleText"
as="textarea"
size="lg"
rows="1"
value={task.title}
onChange={(event)=> setTitle(event.target.value)}
/>
</Form.Group>
</Form>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group controlId="TaskBodyText">
<Form.Control
as="textarea"
multiple
value={task.textBody}
onChange={(event)=> setTextBody(event.target.value)}
></Form.Control>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button
variant="primary"
value={isPending ? "Saving..." : "Submit"}
onClick={updateTask}
>
Save Changes
<ToastContainer />
</Button>
<Button variant="secondary"
value={isPending ? "Saving..." : "Submit"}
onClick={onHide}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
}
export default TaskModal;
Update your code to below, I have checked it and update the solutions with you.
It will perfectly works everytime.
import React, { useState } from "react";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import Modal from "react-bootstrap/Modal";
import taskAPI from "../../utils/taskAPI";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import "./style.css";
function TaskModal({ onHide, onTaskUpdated, task }) {
const [title, setTitle] = useState(task.title || "");
const [textBody, setTextBody] = useState(task.textBody || "");
const [isPending, setIsPending] = useState(false);
const notify = () => toast.warn("You cannot save an empty task!");
const updateTask=(event)=> {
event.preventDefault();
setIsPending(true);
taskAPI
.updateTask({ title, textBody })
.then((response) => {
response.setTitle("");
response.setTextBody("");
})
.then((response) => {
onTaskUpdated();
setIsPending(false);
})
.catch((error) => {
console.log(error);
setIsPending(false);
notify();
});
}
return (
<>
<Modal show={true} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title>
<Form>
<Form.Group controlId="TaskTitleText">
<Form.Control
className="TaskTitleText"
as="textarea"
size="lg"
rows="1"
value={title}
onChange={(event)=> setTitle(event.target.value)}
/>
</Form.Group>
</Form>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group controlId="TaskBodyText">
<Form.Control
as="textarea"
multiple
value={textBody}
onChange={(event)=> setTextBody(event.target.value)}
></Form.Control>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button
variant="primary"
value={isPending ? "Saving..." : "Submit"}
onClick={updateTask}
>
Save Changes
<ToastContainer />
</Button>
<Button variant="secondary"
value={isPending ? "Saving..." : "Submit"}
onClick={onHide}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
}
export default TaskModal;
Note that you are setting the value of your Form.Control to task.textBody and not textBody. task.Textbody is a prop that is passed into your element from the parent element, while taskBody is the local state (from useState("")) that you are actually updating in your onChange function.
You probably want to manage the state in a parent component, in which case you can pass down an onChange method that this component can call whenever the text changes.

React app showing form in dialog results in a "findDOMNode is deprecated" error

I have a react app, the parent component has a button which when clicked shows a simple dialog with one text input and a submit button. Strict mode is enabled. There are two issues
The form input is set to show an initial value (formik initialValues is set) in the input but that is not being set
When the button is clicked I see an error in the console;
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
The dialog component comes from Material UI and the form comes from Formik. I've created a simple repro here. The error is in the dev tools console. What would cause that error and why is the value not initialising?
Here's the parent component;
import React, { useState } from "react";
import { Button, Typography } from "#material-ui/core";
import ProfileEditor from "./ProfileEditor";
function ProfileManager() {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
return (
<div>
<Typography variant="h5">Profile Manager</Typography>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open profile editor dialog
</Button>
<ProfileEditor open={open} onClose={handleClose}></ProfileEditor>
</div>
);
}
export default ProfileManager;
and the dialog component displayed when the button is clicked in the component above;
import React from "react";
import {
Button,
Dialog,
DialogContent,
LinearProgress,
TextField
} from "#material-ui/core";
import { Formik, Form } from "formik";
interface Props {
open: boolean;
onClose: () => void;
}
function ProfileEditor(props: Props) {
return (
<Dialog open={props.open}>
<DialogContent>
<Formik
// initial value not being displayed !!! 😢
initialValues={{
firstName: "Billy"
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
>
{({ submitForm, isSubmitting }) => (
<Form>
<TextField name="firstName" type="text" label="First name" />
{isSubmitting && <LinearProgress />}
<br />
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
>
Submit
</Button>
<Button variant="contained" onClick={props.onClose}>
Close
</Button>
</Form>
)}
</Formik>
</DialogContent>
</Dialog>
);
}
export default ProfileEditor;
You need to include a value prop to the form field to have it initialized properly.
{({ submitForm, isSubmitting, values }) => (
<Form>
<TextField
name="firstName"
type="text"
label="First name"
value={values.firstName} /* you need this prop */
/>
...
CodeSandBox: https://codesandbox.io/s/so-react-formik-inside-material-dialog-sfq4e?file=/ProfileEditor.tsx
Regarding your issue on the console, I'm not entirely sure at this point what is causing it, but if it bothers you or is causing additional problems, perhaps you can opt to move out of strict mode
<React.Fragment>
<ProfileManager></ProfileManager>
</React.Fragment>

React - Form submit button in parent Modal

I have a Semantic UI Form:
import {Form} from 'semantic-ui-react';
<MyForm>
<Form onSubmit={_handleSubmit}>
<Form.Input name="myInput" label="My Label" value="" />
<Form.Group>
<Form.Button>Submit</Form.Button>
</Form.Group>
</Form>
</MyForm>
This form can be displayed inside a modal, or directly in a standard view in my app
My modal looks like this:
import {Button, Modal} from 'semantic-ui-react';
<Modal open={true} size="large" centered>
<Modal.Header>My Label</Modal.Header>
<Modal.Content>
<MyForm />
</Modal.Content>
<Modal.Actions>
<Button className="close-button">Cancel</Button>
{/* Insert submit button here*/}
</Modal.Actions>
</Modal>
This simple approach is working.
What I would like to do, is to have the submit button inside the Modal.Actions section when it's displayed in a modal, and keep it right after the input otherwise.
I don't know how to tell my form that the submit button is somewhere in its parent.
I finally managed to do it using a ref.
The idea is to create a ref in the form, pointing to the submit function and having a function in props to transmit this ref to my modal.
Modal:
import {Button, Modal} from 'semantic-ui-react';
const [submitFunc, setSubmitFunc] = useState();
const submitForm = () => {
if (submitFunc) {
submitFunc.current();
}
};
<Modal open={true} size="large" centered>
<Modal.Header>My Label</Modal.Header>
<Modal.Content>
<MyForm setSubmitFunc={setSubmitFunc} />
</Modal.Content>
<Modal.Actions>
<Button>Cancel</Button>
<Button onClick={submitForm}>Submit</Button>
</Modal.Actions>
</Modal>
Form:
function EditRecordForm({setSubmitFunc}) {
const submitRef = useRef(null);
useEffect(() => {
if (!!setSubmitFunc) {
setSubmitFunc(submitRef);
}
});
const handleSubmit = () => {
// Do whatever you need to retrieve form values and submit it
}
submitRef.current = handleSubmit;
return (
<MyForm>
<Form onSubmit={_handleSubmit}>
<Form.Input name="myInput" label="My Label" value="" />
<Form.Group>
<Form.Button>Submit</Form.Button>
</Form.Group>
</Form>
</MyForm>
)
}
What you can do is, you can associate the form with the button in the modal actions using a form id. Here is how you do it :-
Form:
<MyForm>
<Form id={'my-form'} onSubmit={_handleSubmit}>
{/*Form Elements}
</Form>
</MyForm>
Modal:
<Modal.Actions>
<Button>Cancel</Button>
<Button type={'submit'} form={'my-form'}>Submit</Button>
</Modal.Actions>
Following link is the tweet by the creator of chakr-ui telling the same method to join the form in a side drawer which needs to be connected to the button in the drawer footer.
https://twitter.com/thesegunadebayo/status/1330866834636201987?lang=en

Resources