React. Passing Functions For A Modal - reactjs

I am trying to Close a modal from a child component of the modal component, my problem is, I have to declare a function on the grand parent component for the gradchild component and my code to update it isn't working. My test app uses react-bootstrap and currently has no store lib. What I am trying todo is nest a "form" component within a "modal" component and pass the closing function to the form component within the modal component all on a grand parent. the code is as follows:
This is the main component which contains my Modal Component and Form Component
import { useEffect } from "react";
import { GetAllGrades } from "../../../Services/GradeApi";
import FormModal from "../../Layout/Modal/FormModal";
import AddGradeModal from "./AddGradeModal";
import EditGradeModal from "./EditGradeModal";
import GradeForm from "./GradeForm";
const AllGrades = () => {
const [grades, setGrades] = GetAllGrades();
useEffect(() => {
setGrades();
}, [grades, setGrades]);
return (
<table className="table .table-striped ">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">KYU</th>
<th scope="col">Required Session Time</th>
<th>
</th>
<th></th>
</tr>
</thead>
<tbody>
{grades?.map((grade) => (
<tr key={grade.id}>
<th>{grade.name}</th>
<td>{grade.kyu}</td>
<td>{grade.requiredLessionTime}</td>
<td>
</td>
<td>
<FormModal ModalTitle="Form Modal" TriggerButtonText="Test">
<GradeForm Grade={grade} ViewOnly={false}>
closeModal={????} </GradeForm> //not sure how not to set this property for the callback function here
</FormModal>
</td>
</tr>
))}
</tbody>
</table>
);
};
export default AllGrades;
Modal Component - I am trying to add its "handlClose" function to the child component so it can call and close the modal once the for is submitted.
import React, { useEffect, useState } from "react";
import { Button, Modal } from "react-bootstrap";
type FormModalProps = {
ModalTitle: string;
TriggerButtonText: string;
children?: React.ReactNode;
};
const FormModal = (props: FormModalProps) => {
const { ModalTitle, TriggerButtonText, children } = props;
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const childrenWithProps = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
closeModal: { handleClose },
});
}
return child;
}); // update the children with the correct function
return (
<>
<Button variant="primary" onClick={handleShow}>
{TriggerButtonText}
</Button>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>{ModalTitle}</Modal.Title>
</Modal.Header>
<Modal.Body>{childrenWithProps}</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
};
export default FormModal;
this is the form - on the submit function I want to close the modal, so I pass the "handleClose" function from the parent.
import { useForm, Controller } from "react-hook-form";
import { yupResolver } from "#hookform/resolvers/yup";
import { Grade, GradeFormProps } from "../../../Interface";
import { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { GradeValidationSchema } from "../../../Services/ValidationSchemas/GradeValidation";
const GradeForm = (props: GradeFormProps) => {
const [grade, setGrade] = useState<Grade>(props.Grade);
const { Grade, ViewOnly, closeModal } = props;
const {
handleSubmit,
control,
formState: { errors },
} = useForm<Grade>({
resolver: yupResolver(GradeValidationSchema),
defaultValues: grade,
});
const onSubmit = (data: Grade) => {
if (data.id === undefined) {
// AddMember({ Grade: data });
} else {
// UpdateMember({ Grade: data });
}
console.log("close Form");
closeModal && closeModal(null);
// closeModal.setShow(true);
};
useEffect(() => {
setGrade(Grade);
}, [setGrade, grade, Grade]);
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<fieldset disabled={ViewOnly}>
<Form.Group className="mb-3" controlId="name">
<Form.Label>Licence Number</Form.Label>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field: { onChange, onBlur, value, ref } }) => (
<Form.Control
onChange={onChange}
value={value}
ref={ref}
placeholder="Enter Name"
/>
)}
/>
{errors.name && (
<div className="text-danger">{errors.name?.message}</div>
)}
</Form.Group>
<Form.Group className="mb-3" controlId="kyu">
<Form.Label>KYU</Form.Label>
<Controller
control={control}
name="kyu"
render={({ field: { onChange, onBlur, value, ref } }) => (
<Form.Control
onChange={onChange}
value={value}
ref={ref}
placeholder="Enter Kyu"
/>
)}
/>
{errors.kyu && (
<div className="text-danger">{errors.kyu?.message}</div>
)}
</Form.Group>
<Form.Group className="mb-3" controlId="requiredLessionTime">
<Form.Label>Last Name</Form.Label>
<Controller
control={control}
name="requiredLessionTime"
render={({ field: { onChange, onBlur, value, ref } }) => (
<Form.Control
onChange={onChange}
value={value}
ref={ref}
placeholder="Enter required Session Time"
/>
)}
/>
{errors.requiredLessionTime && (
<div className="text-danger">
{errors.requiredLessionTime?.message}
</div>
)}
</Form.Group>
{!props.ViewOnly && (
<Button variant="primary" type="submit">
Submit
</Button>
)}
</fieldset>
</Form>
);
};
export default GradeForm;
Props for the Form
export interface GradeFormProps{
Grade: Grade,
ViewOnly: boolean,
closeModal: (params:any)=>any ;
}
any help would be appreciated.

I think, you just have a syntax issue in your FormModal component. handleClose shouldn't be wrapped in an object.
import React, { useEffect, useState } from "react";
import { Button, Modal } from "react-bootstrap";
type FormModalProps = {
ModalTitle: string;
TriggerButtonText: string;
children?: React.ReactNode;
};
const FormModal = (props: FormModalProps) => {
const { ModalTitle, TriggerButtonText, children } = props;
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const childrenWithProps = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
closeModal: handleClose
});
}
return child;
}); // update the children with the correct function
return (
<>
<Button variant="primary" onClick={handleShow}>
{TriggerButtonText}
</Button>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>{ModalTitle}</Modal.Title>
</Modal.Header>
<Modal.Body>{childrenWithProps}</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
};
export default FormModal;
In the AllGrades component, you don't need to have closeModal in the JSX but this could be an old code fragment.
Additionally, I would recommend to rename the closeModal prop of GradeForm into something like onSuccess or onFinish, as the form doesn't know that it is rendered within a modal or not.

Related

React-Bootsrap Multiple Modals

I have a navbar with the usual login and sign-up buttons. When I click the relevant link, a modal with the form should pop-up allowing the user to login or sign-up. At the moment, neither of the pop-ups seem to work. I have been following React-Bootstrap Multiple Modal and have been using the chosen answer to try and implement the modals.
The handle functions are in my app.jsx:
import React, {useEffect, useState} from 'react'
import {useRouter} from 'next/router'
import {SessionProvider, useSession} from 'next-auth/react'
import {SSRProvider} from 'react-bootstrap'
import 'application.css';
import Navigation from "../components/Navigation";
import Login from "../components/Login";
import SideNav from "../components/SideNav";
import SignUp from "../components/SignUp";
function MyApp({Component, pageProps}) {
let id = '';
const [show, setShow] = useState(null)
function handleClose() {
setShow({show: id});
}
function handleShow(id) {
setShow({show: id});
}
const [showSideBar, setShowSideBar] = useState(false)
const toggleSideMenu = () => setShowSideBar((prev) => !prev);
return (
<SessionProvider session={pageProps.session}>
<SSRProvider>
<SideNav showSideBar={showSideBar} />
<Navigation
handleShow={handleShow}
handleClose={handleClose}
toggleSideMenu={toggleSideMenu}
/>
<Login
show={show}
handleShow={handleShow}
handleClose={handleClose}
/>
<SignUp
show={show}
handleShow={handleShow}
handleClose={handleClose}
/>
{Component.auth
? <Auth><Component {...pageProps} /></Auth>
: <Component {...pageProps} />
}
</SSRProvider>
</SessionProvider>
)
}
function Auth({children}) {
// #ts-ignore
const [session, loading] = useSession()
const isUser = !!session?.user
const router = useRouter()
useEffect(() => {
if (loading) return // Do nothing while loading
if (!isUser) router.push('/login')
// If not authenticated, force log in
}, [isUser, loading])
if (isUser) {
return children
}
// Session is being fetched, or no user.
// If no user, useEffect() will redirect.
return <div>Loading...</div>
}
export default MyApp
I pass the show, handleshow, handleclose to each component. In the navigation the buttons look like this:
<Nav.Link href="#" onClick={() => handleShow('login')}><FontAwesomeIcon icon={solid('sign-in-alt')}/><span> Login</span></Nav.Link>
<Nav.Link href="#" onClick={() => handleShow('signup')}><FontAwesomeIcon icon={solid('user-plus')}/><span> Sign up</span></Nav.Link>
And then finally, my Login.jsx with a modal:
import {getCsrfToken, signIn, useSession} from "next-auth/react";
import Router from "next/router";
import { Modal, CloseButton, Form } from 'react-bootstrap';
import Link from "next/link";
import { useState } from "react";
function Login({csrfToken, show, handleClose, handleShow, props}) {
const [error, setError] = useState(false);
//setShow(prev => !prev);
const handleSubmit = async (e) => {
e.preventDefault();
const res = await signIn('credentials', {
redirect: false,
email: e.target.email1.value,
password: e.target.password1.value,
callbackUrl: `/dashboard`,
});
if (res?.error) {
setError(true);
} else {
Router.push('/dashboard');
}
}
return (
<Modal show={show === "login"} onHide={handleClose} fade={false}>
<Modal.Header className={"modal-dark bg-dark"}>
<Modal.Title><h1>Log In</h1></Modal.Title>
<CloseButton variant="white" onClick={handleClose}/>
</Modal.Header>
<Modal.Body className={"modal-dark bg-dark"}>
<form noValidate onSubmit={(e) => handleSubmit(e)}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken}/>
<Form.Group className="mt-3">
<Form.Control
id="email-address1"
name="email1"
type="email"
autoComplete="email"
required
className=""
placeholder="Email address"
/>
</Form.Group>
<Form.Group className="mt-3">
<Form.Control
id="password1"
name="password1"
type="password"
autoComplete="current-password"
required
placeholder="Password"
/>
</Form.Group>
<button type={"submit"} className={"btn orange-button mt-3"}>Login</button>
<div>
or <Link href='/signup'>sign up</Link>
</div>
{error && <div className="bg-red-300 p-2 text-white rounded">Wrong email or password</div>}
</form>
</Modal.Body>
<Modal.Footer className={"modal-dark bg-dark"}>
<button onClick={handleClose}>
Close
</button>
</Modal.Footer>
</Modal>
);
}
export default Login;
In the developer tools looking at the React components tab, it is sending the correct information.
The props all seem to be there, the modal just doesn't seem to pop-up. Thanks

How to prevent re-rendering on onChange Event of input field

/**
Make sure we keep the same functionality, without causing a re-render
on every keypress when typing in the input.
*/
import React, { useState } from "react";
export const Example: React.FC = () => {
const [userInput, setUserInput] = useState("");
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setUserInput(event.target.value);
}
console.log("Example renders");
return (
<>
<h1>Send us some feedback</h1>
<div>
<input type="text" onChange={handleChange} value={userInput} />
<button
type="submit"
onClick={() => {
alert(userInput);
}}
>
Submit
</button>
</div>
</>
);
};

Can't type in react input field

I have a simple form with an input field that I can't type on. I first thought the problem was with the onChange or the value props that were setting the input to readonly, but the fact is that I cant type with the browser suggestions and the state updates perfectly (See gif here) it's just that I won't let me type with the keyboard, even after reloading the page.
I also have a Login page that works perfectly except when I log out and redirect back to that page, it won't work until I reload the page, now it will work.
<input
value={name}
onChange={handleChange}
name="name"
/>
const [name, setName] = useState("");
const handleChange = (e:any) => {
setName(e.target.value);
}
Weird thing is that it's in like a readonly state but when I use browser suggestions it works and updates the state.
Here is the whole component:
import React, { useEffect, useState } from 'react';
import { useForm } from '../../utils/useForm';
import { CubeType } from '../../interfaces';
//import useStore from '../store/Store';
import { Modal, Button, Row, Col, FormGroup, FormLabel, FormControl } from 'react-bootstrap';
type Props = {
show: Boolean,
onClose: () => void,
cubeTypes: CubeType[]
};
const ModalTimelist = (props: Props) => {
//const store = useStore();
const [values, handleChangee] = useForm({ cubeType: 1, name: '' });
const [name, setName] = useState("");
const handleChange = (e:any) => {
setName(e.target.value);
}
useEffect(() => {
const modal = document.getElementsByClassName('modal')[0];
if(modal) modal.removeAttribute('tabindex');
}, [props.show]);
return (
<>
<Modal show={props.show} onHide={ props.onClose }>
<Modal.Header>
<Modal.Title>Timelist { name }</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row>
<Col md="3">
<FormGroup>
<FormLabel>Cube Type</FormLabel>
<select
value={values.cubeType}
onChange={ handleChangee }
className="form-select"
name="cubeType"
>
{props.cubeTypes.map((it, idx) => {
return (<option value={ idx } key={"cube"+idx}>{it.name}</option>);
}) }
</select>
</FormGroup>
</Col>
<Col md="9">
<FormGroup>
<FormLabel>Name</FormLabel>
<FormControl
value={name}
onChange={handleChange}
name="name"
/>
</FormGroup>
</Col>
</Row>
</Modal.Body>
<Modal.Footer>
<Button variant="success" onClick={() => props.onClose()}>
Save
</Button>
<Button variant="outline-danger" onClick={() => props.onClose()}>
Cancel
</Button>
</Modal.Footer>
</Modal>
</>
);
}
export default ModalTimelist;
value of input must be the state value otherwise it will not change use this code
const App = () => {
const [name,setName] = useState("")
const handle = ({target:{value}}) => setName(value)
return <input
value={name}
onChange={handle}
name="name"
/>
}
Use a debounce for setting name on state.
Example:
const handleChange = (e:any) => {
debounce(() => { setName(e.target.value) }, 300);
}
I tried the code and it works fine I think you should change the browser
and if you want
change this
const ModalTimelist = (props: Props) => {
with
const ModalTimelist:React.FC<Props> = (props) => {
Names specified by you in input field attributes must be same as useState names. Otherwise this problem occurs.
Example:
<input type={"text"} className="form-control" placeholder='Enter your User Name' name="username" value={username} onChange={(e)=>onInputChange(e)}/>
In name="username" , username spell must be same as the spell you used in State.

Console Log from Props

I'm confused as I have managed to get my data to be logged via different means, but confused as to why when I use props for the data (rather than repeating code) it will not log the input.
For reference, I have a field component that will take props to drive what my react-hook-form TextField will request. I'd like to expand on the component but until it logs my data, I cannot proceed!
Below is the code that actually logs my data:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField, Button } from "#material-ui/core/";
const NewRequest = () => {
const { register, handleSubmit, control } = useForm();
const onSubmit = (data) => console.log(data);
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name='firstName'
render={({ field: { onChange, onBlur, value, name, ref } }) => (
<TextField
label='First Name'
variant='filled'
size='small'
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
/>
)}
/>
<br />
<br />
<Button type='submit' variant='contained'>
Submit
</Button>
</form>
</div>
);
};
export default NewRequest;
I have then moved the Controller, TextField to create a component:
import React from "react";
import { Controller, useForm } from "react-hook-form";
import { TextField } from "#material-ui/core/";
const TextFieldComponent = (props) => {
const { name, label, size, variant } = props;
const { control } = useForm();
return (
<div>
<Controller
control={control}
name={name}
render={({ field: { onChange, onBlur, value, ref } }) => (
<TextField
label={label}
variant={variant}
size={size}
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
/>
)}
/>
</div>
);
};
export default TextFieldComponent;
Which I am using inside of another component (to generate a full form) and passing through my props (I will make a different component for Button, but for now it is where it is):
import React from "react";
import { useForm, Controller } from "react-hook-form";
import TextFieldComponent from "./form-components/text-field";
import { Button } from "#material-ui/core/";
const NewRequest= () => {
return (
<div>
<TextFieldComponent
name='firstName'
label='First Name'
size='small'
variant='filled'
/>
<br />
<br />
<Button type='submit' variant='contained'>
Submit
</Button>
</div>
);
};
export default NewRequest;
Now pushing that component into an index.js file to render a form:
import React from "react";
import NewVendorForm from "../components/new-vendor-request";
import { useForm } from "react-hook-form";
const Home = () => {
const { handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<NewVendorForm />
</form>
);
};
export default Home;
I'm stumped as to why this way would
a) customise my TextField in my form as intended
b) but not log my data as requested
I'm sure there is a very valid, basic reason as to why and it is my lack of understanding of console logging, but am in need of help to resolve!
Many thanks in advance.
The issue is that, in the refactored code, you're calling useForm twice, each of which generates a different control and data. You probably want to call useForm at the top level only, and pass in whatever you need (in particular control) to the form fields.
const Home = () => {
const { handleSubmit, control } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<NewVendorForm control={control} />
</form>
);
};
const NewRequest= ({control}) => {
return (
<div>
<TextFieldComponent
name='firstName'
label='First Name'
size='small'
variant='filled'
control={control}
/>
<br />
<br />
<Button type='submit' variant='contained'>
Submit
</Button>
</div>
);
};
const TextFieldComponent = (props) => {
const { name, label, size, variant, control } = props;
return (
<div>
<Controller
control={control}
name={name}
render={({ field: { onChange, onBlur, value, ref } }) => (
<TextField
label={label}
variant={variant}
size={size}
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
/>
)}
/>
</div>
);
};

onClick call component with props

Following is Navigation link with onclick event. I couldn't figure out how to call ItemModal component when navigation with onclick event is called. I have tried with following code, it's not working.
<strong onClick={() => <ItemModal modal={true} isAuthenticated={true} />}> Add Item </strong>
Below is ItemModal components with reactstrap modal to add new items.
ItemModal.js
import React, { Component } from "react";
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input,
} from "reactstrap";
import { connect } from "react-redux";
import { addItem } from "../actions/itemActions";
import PropTypes from "prop-types";
class ItemModal extends Component {
state = {
modal: false,
name: "",
};
static propTypes = {
isAuthenticated: PropTypes.bool,
};
toggle = () => {
this.setState({
modal: !this.state.modal,
});
};
onChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
onSubmit = (e) => {
e.preventDefault();
const newItem = {
name: this.state.name,
};
// Add item via addItem action
this.props.addItem(newItem);
//Close Modal
this.toggle();
};
render() {
return (
<div>
{this.props.isAuthenticated ? (
<Button
color="dark"
style={{ marginBottom: "2rem" }}
onClick={this.toggle}
>
Add Item
</Button>
) : (
<h4 className="mb-3 ml-4"> Please log in to manage items</h4>
)}
<Modal isOpen={this.state.modal} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}> Add to Shopping List</ModalHeader>
<ModalBody>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="item">Item</Label>
<Input
type="text"
name="name"
id="item"
placeholder="Add shopping item"
onChange={this.onChange}
/>
<Button color="dark" style={{ marginTop: "2rem" }} block>
Add Item
</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
const mapStateToProps = (state) => ({
item: state.item,
isAuthenticated: state.auth.isAuthenticated,
});
export default connect(mapStateToProps, { addItem })(ItemModal);
How I can call component on click so that reactstrap model (ItemModal) is opened?

Resources