I'm trying to make a Formik wrapper which takes children as props and would render anything put inside. There are a couple forms to make which take different initial values and validation schema etc. The only thing in common thing is the grid layout. The goal is to have the access to Formik props like values, errors etc. in the child component and I have no idea how to pass it to its child. The form fields don't even show up.
The wrapper:
import React from 'react';
import { Formik, FormikConfig, FormikValues } from "formik";
import { Col, Layout, Row } from "antd";
const FormContainer: React.FC<FormikConfig<FormikValues>> = ({ children, ...props }) => {
return <Formik
{...props}
>
{props => (
<Layout>
<Row style={{ height: "100vh", display: "flex", alignItems: "center" }}>
<Col span={12}>
<Layout>
{/*this will be replaced with some background image*/}
<pre>{JSON.stringify(props.values, null, 2)}</pre>
<pre>{JSON.stringify(props.errors, null, 2)}</pre>
</Layout>
</Col>
<Col span={12}>
<Layout>
{/*here goes goes a Form from a different components*/}
{children}
</Layout>
</Col>
</Row>
</Layout>
)}
</Formik>
};
export default FormContainer;
I must be doing something wrong. I am unable to get any Formik props/values from anywhere else when I wrap FormContainer around anything.
My form example (so far):
import React from "react";
import { Field, Form } from "formik";
import { Col, Form as AntForm, Icon, Input, Row } from "antd";
import { initialValues, validationSchema } from "./fieldValidation";
import FormContainer from "../../../containers/FormContainer/FormContainer";
const RegisterPage: React.FC = () => {
return (
<FormContainer
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(data, { setSubmitting }) => {
setSubmitting(true);
setTimeout(() => {
alert(JSON.stringify(data, null, 2));
setSubmitting(false);
}, 5000);
}}
>
{({touched, errors}) => (
<Form>
<Row gutter={[8, 8]}>
<Col span={12}>
<AntForm.Item
help={touched.firstName && errors.firstName ? errors.firstName : ""}
validateStatus={touched.firstName && errors.firstName ? "error" : undefined}
>
<Field
name="firstName"
prefix={<Icon type="solution" style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="First name"
as={Input}
/>
</AntForm.Item>
</Col>
<Col span={12}>
<AntForm.Item
help={touched.lastName && errors.lastName ? errors.lastName : ""}
validateStatus={touched.lastName && errors.lastName ? "error" : undefined}
>
<Field
name="lastName"
prefix={<Icon type="solution" style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="Last name"
as={Input}
/>
</AntForm.Item>
</Col>
</Row>
</Form>
)}
</FormContainer>
);
};
export default RegisterPage;
I'm stuck. What am I doing wrong here?
Here's how to pass the prop "propsToPass" from the parent to all his direct children:
const Parent = props => {
const { children } = props;
const childrenWithExtraProp = React.Children.map(children, child =>
React.cloneElement(child, { propsToPass: "toChildren" })
);
return <div>{childrenWithExtraProp}</div>;
};
export default Parent;
So in this case, both children will have the prop "propsToPass"
<Parent>
{/* this.props.propsToPass will be available in this component */}
<Child></Child>
{/* this.props.propsToPass will be available in this component */}
<AnotherChild></AnotherChild>
</Parent>
You could do the same for your form.
I don't see like rendering Formik as children is good idea here, especially that you are supposed to render one form in such FormWrapper. I would use render props here, so here is basic example how you can do it.
Anyway, I still can't get your concept of re-inventing FormWrapper if Formik provides its own wrapper:
https://jaredpalmer.com/formik/docs/api/formik
interface FormWrapperProps extends FormikConfig<FormikValues> {
renderForm(props: FormWrapperProps): React.ReactNode
}
export const RegisterForm = (props: FormWrapperProps) => (
<form>
<input type="text"/>
<input type="text"/>
</form>
)
const FormWrapper: React.FC<FormWrapperProps> = (props) => {
return (
<div className="layout">
{/*here goes goes a Form from a different components*/}
{props.renderForm(props)}
</div>
)
}
const FormPage = () => {
const props = {} as FormWrapperProps
return (
<FormWrapper
{...props}
renderForm={(props: FormWrapperProps) => <RegisterForm {...props} />}
/>
)
}
Related
I have a datagrid table in which I'm getting my database data from an API call and I have written the table code in one file. I also have a search functionality where you can search for a particular record inside the table, but this search code is in another file. I am having difficulty of passing my state variable containing the search parameter from my search file to the table file. I have separated all my components in different pages since it'd be easier to structure them using a grid in my App.js. How do I get my search query to my table file next?
My search code:
export default function SearchInput() {
const [searchTerm, setSearchTerm] = React.useState('');
return (
<Grid item xs={3}>
<Box mt={1.6}
component="form"
sx={{
'& > :not(style)': { m: 1, width: '20ch', backgroundColor: "white", borderRadius: 1},
}}
noValidate
autoComplete="off"
>
<TextField
placeholder="Search Customer ID"
variant="outlined"
size="small"
sx={{input: {textAlign: "left"}}}
onChange={(event) => {
setSearchTerm(event.target.value);
console.log(searchTerm);
}}
/>
</Box>
</Grid>
);
}
My table code:
export default function DataTable() {
const [pageSize, setPageSize] = React.useState(10);
const [data, setData] = React.useState([]);
useEffect(async () => {
setData(await getData());
}, [])
return (
<div style={{ width: '100%' }}>
<DataGrid
rows={data}
columns={columns}
checkboxSelection={true}
autoHeight={true}
density='compact'
rowHeight='40'
headerHeight={80}
disableColumnMenu={true}
disableSelectionOnClick={true}
sx={datagridSx}
pageSize={pageSize}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
rowsPerPageOptions={[5, 10, 15]}
pagination
/>
</div>
);
}
App.js
function App() {
return (
<div className="App">
<Container maxWidth="false" disableGutters="true">
<Grid container spacing={0}>
<ABClogo />
<HHHlogo />
</Grid>
<Grid container spacing={0}>
<LeftButtonGroup />
<SearchInput />
<RightButtonGroup />
</Grid>
<Grid container spacing={0}>
<DataTable />
<TableFooter />
</Grid>
</Container>
</div>
);
}
Here is a minimal example using createContext(), and useReducer() to lift up state and share it between components, similar to what you are after, but as jsNoob says, there are multiple options. This is one I'm comfortable with.
The concept is explained here: https://reactjs.org/docs/context.html
Essentially you can create 'global' state at any point in your component tree and using Provider / Consumer components, share that state and functionality with child components.
//Main.js
import React, { createContext, useContext, useReducer } from 'react';
const MainContext = createContext();
export const useMainContext => {
return useContext(MainContext);
}
const mainReducer = (state, action) => {
switch(action.type){
case: 'SOMETHING':{
return({...state, something: action.data});
}
default:
return state;
}
}
export const Main = () => {
const [mainState, mainDispatch] = useReducer(mainReducer, {something: false});
const stateOfMain = { mainState, mainDispatch };
return(
<MainContext.Provider value={stateOfMain}>
<MainContext.Consumer>
{() => (
<div>
<Nothing />
<Whatever />
</div>
)}
</MainContext.Consumer>
</MainContext.Provider>
)
}
Then you can have the other components use either or both of the main state and dispatch.
//Nothing.js
import {mainContext} from './Main.js'
const Nothing = () => {
const { mainState, mainDispatch } = useMainContext();
return(
<button onClick={() => {mainDispatch({type: 'SOMETHING', data: !mainState.something})}}></button>
)
}
Clicking the button in the above file, should change the display of the below file
//Whatever.js
import {mainContext} from './Main.js'
const Whatever = () => {
const { mainState } = useMainContext();
return(
<div>{mainState.something}</div>
);
}
I have these tabs to go to the next step. However, once clicking submits, this will go to the correct page, but this will also remove the tab. How can I fix this?
I have recreated this in codesandbox: https://codesandbox.io/s/dawn-cloud-uxfe97?file=/src/App.js
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography component="span">{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
const Ordering = () => {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
label="Step 1"
{...a11yProps(0)}
component={Link}
to={`/step1`}
/>
<Tab label="Step 2" {...a11yProps(1)} />
</Tabs>
</Box>
<TabPanel value={value} index={0}>
<Step1 />
</TabPanel>
<TabPanel value={value} index={1}>
<Step2 />
</TabPanel>
</div>
);
};
export default Ordering;
Step1.js
Navigating this to the step2 component does go to the next page, but this will also remove the tab
import { useNavigate } from "react-router-dom";
const Step1 = () => {
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
navigate("/Step2");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="submit" />
</form>
</div>
);
};
export default Step1;
Step2.js
const Step2 = () => {
return <div>Step2</div>;
};
export default Step2;
In your case you're not nesting your routes properly. So, For nested route you need to define a route inside another one. Refer this page https://reactrouter.com/docs/en/v6/api#routes-and-route for more information.
Getting back to your question. You need to update your code in two place. First how the routes are defined.
<Route path="/page" element={<Page />}>
<Route path="step1" element={<Step1 />} />
<Route path="step2" element={<Step2 />} />
</Route>
Once, your routes are updated. You are linking your panels based on route. So, you need not define them again in your Page component. You can remove those component from there and just add code so that when tab is click you change your navigation bar url. Sadly Tab of mui doesn't support component property https://mui.com/api/tab/ . You have to do that manually. You can use useNavigate provided by react router dom. Your updated Page component would look like this
I have added comment // This is added. To see where I've made changes. Just in 2 places changes are required.
import React, { useState, useEffect } from "react";
import { Box, Tab, Typography, Tabs } from "#mui/material";
import PropTypes from "prop-types";
import Step1 from "./Step1";
import Step2 from "./Step2";
import { Link } from "react";
// hook is imported
import { useNavigate } from "react-router-dom";
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography component="span">{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
const paths = ['/page/step1', '/page/step2']
const Ordering = () => {
const [value, setValue] = React.useState(0);
//This is added
const navigate = useNavigate()
const handleChange = (event, newValue) => {
setValue(newValue);
// This is added
navigate(paths[newValue])
};
return (
<div>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
label="Step 1"
{...a11yProps(0)}
component={Link}
to={`/step1`}
/>
<Tab label="Step 2" {...a11yProps(1)} />
</Tabs>
</Box>
{/* Removed tab panels from here */}
</div>
);
};
export default Ordering;
I am passing methods as a prop in this form I am making with react-hook-form.
Its giving me (TypeError: props.render is not a function) when Controller is added in from react-hook-form. I cannot find any solutions online so any help is appreciated.
import { useForm, FormProvider } from 'react-hook-form';
import FormInput from './CustomTextField';
const AddressForm = () => {
const methods = useForm();
return (
<>
<FormProvider {...methods}>
<form onSubmit=' '>
<Grid container spacing={3}>
<FormInput required name='firstName' label='First name' />
</Grid>
</form>
</FormProvider>
</>
);
};
import { useFormContext, Controller } from 'react-hook-form';
const FormInput = ({ name, label, required }) => {
const { control } = useFormContext();
return (
<>
<Controller
as={TextField}
name={name}
control={control}
label={label}
fullWidth
required={required}
/>
<>
);
};
export default FormInput;
Was stuck in somewhat similar problem,
you can try following changes in FormInput function:
import React from 'react';
import { TextField, Grid } from '#material-ui/core';
import { useFormContext, Controller } from 'react-hook-form';
const FormInput = ({ name, label, required}) => {
const { control } = useFormContext();
const isError = false;
return (
<>
<Controller
control={control}
name={name}
render = {({ field})=> (
<TextField
fullWidth
label={label}
required
/>
)}
/>
</>
);
}
export default FormInput;
hope this helps, else you can go through the docs
I had this problem (TypeError: props.render is not a function) with react-hook-form + react-select when I tried to reuse the same Controller component from an old project, and I fixed this way:
Unchanged:
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
From:
<Controller
name="languages"
control={control}
rules={{ required: true }}
as={Select}
options={props.languageOptionsToSelect}
defaultValue={props.languageDefaultValueToSelect}
isMulti
/>;
To:
<Controller
name="languages"
control={control}
rules={{ required: true }}
render={({ field }) => (
<Select
{...field}
options={props.languageOptionsToSelect}
defaultValue={props.languageDefaultValueToSelect}
isMulti
/>
)}
/>;
It seems render prop in Controller component is required now.
This problem is arising either because you update your react-hook-form or new to react-hook-form
You just need to use render prop in Controller component
<Controller
render={({ field }) => (
<input
onChange={(e) => field.onChange(transform.output(e))}
value={transform.input(field.value)}
/>
)}
/>
or if you are using a third party Form library
import { Input, Select, MenuItem } from "#material-ui/core";
<Controller
render={({ field }) => (
<Select {...field}>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
</Select>
)}
control={control}
name="select"
defaultValue={10}
/>
Try this one!
<Controller
render={({ field }) => <TextField {...field} />}
name={name}
control={control}
label={label}
fullWidth
required={required}
/>
Add render as a prop in the Controller Component. Refer to the docs here
import React from 'react'
import { TextField, Grid } from '#material-ui/core'
import { useFormContext, Controller } from 'react-hook-form'
const FormInput = ({ name, label, required }) => {
const { control } = useFormContext()
return (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={name}
render = {({ field})=> (
<TextField
fullWidth
label={label}
required
/>
)}
/>
</Grid>
)
}
export default FormInput
It now requires a render props, try this on MUI:
<Controller
render={({ field }) => (
<TextField {...field} label={label} required={required}/>)}
control={control}
fullWidth
name={name}
/>
I'm trying to access "props" from a component for which I'm passing an object. I'm a bit lost with JS here ; basically what I'm trying to do is to build a Master/Detail view (so show/hide 2 different components based on user clicks on a table).
How can I access "props" from the object rowEvent once a user clicks on a table row ?
const rowEvents = {
onClick: (e, row, rowIndex) => {
console.log(row.itemId);
//this.currentItemId= row.itemId; //////////// THIS DOESNT WORK...
}
};
const TableWithSearch = (props) => {
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
return (
<Card>
<CardBody>
<h4 className="header-title">Search and Export</h4>
<p className="text-muted font-14 mb-4">A Table</p>
<ToolkitProvider
bootstrap4
keyField="itemId"
data={props.data}
columns={columns}
search
exportCSV={{ onlyExportFiltered: true, exportAll: false }}>
{props => (
<React.Fragment>
<Row>
<Col>
<SearchBar {...props.searchProps} />
</Col>
<Col className="text-right">
<ExportCSVButton {...props.csvProps} className="btn btn-primary">
Export CSV
</ExportCSVButton>
</Col>
</Row>
<BootstrapTable
{...props.baseProps}
bordered={false}
rowEvents={ rowEvents }
defaultSorted={defaultSorted}
pagination={paginationFactory({ sizePerPage: 5 })}
wrapperClasses="table-responsive"
/>
</React.Fragment>
)}
</ToolkitProvider>
</CardBody>
</Card>
);
};
And the component looks like this :
render() {
let show;
if (this.props.currentItemId === null){
show = (<TableWithSearch data={this.props.data} />)
}
else {
show = (<DisplayItem />)
}
return (
<React.Fragment>
<Row>
<Col>
{ show }
</Col>
</Row>
</React.Fragment>
)
}
}
Your issue is a bit complex because you seem to be needing to update the prop currentItemId from parent's parent.
You can solve your issue by doing the following:
Move the declaration of rowEvents objects in side TableWithSearch functional component.
In TableWithSearch component, receive a callback say updateCurrentItemId from parent which updates the currentItemId in the parent
In parent component, the currentItemId is being passed from parent(again). So maintain a state for it.
TableWithSearch Component
const TableWithSearch = (props) => {
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
const {updateCurrentItemId} = props; //<--------- receive the prop callback from parent
const rowEvents = {
onClick: (e, row, rowIndex) => {
console.log(row.itemId);
updateCurrentItemId(row.itemId) // <--------- use a callback which updates the currentItemId in the parent
//this.currentItemId= row.itemId; //////////// THIS DOESNT WORK...
},
};
return (
<Card>
<CardBody>
<h4 className="header-title">Search and Export</h4>
<p className="text-muted font-14 mb-4">A Table</p>
<ToolkitProvider
bootstrap4
keyField="itemId"
data={props.data}
columns={columns}
search
exportCSV={{ onlyExportFiltered: true, exportAll: false }}
>
{(props) => (
<React.Fragment>
<Row>
<Col>
<SearchBar {...props.searchProps} />
</Col>
<Col className="text-right">
<ExportCSVButton
{...props.csvProps}
className="btn btn-primary"
>
Export CSV
</ExportCSVButton>
</Col>
</Row>
<BootstrapTable
{...props.baseProps}
bordered={false}
rowEvents={rowEvents}
defaultSorted={defaultSorted}
pagination={paginationFactory({ sizePerPage: 5 })}
wrapperClasses="table-responsive"
/>
</React.Fragment>
)}
</ToolkitProvider>
</CardBody>
</Card>
);
};
Parent Component
class ParentComp extends React.Component {
state = {
curItemId: this.props.currentItemId
}
updateCurrentItemId = (udpatedCurId) => {
this.setState({
curItemId: udpatedCurId
})
}
render() {
let show;
// if (this.props.currentItemId === null){
if (this.state.curItemId === null){
show = (<TableWithSearch data={this.props.data} updateCurrentItemId={this.updateCurrentItemId}/>)
}
else {
show = (<DisplayItem />)
}
return (
<React.Fragment>
<Row>
<Col>
{ show }
</Col>
</Row>
</React.Fragment>
)
}
}
}
this.props should give you access for class components
In addition you should create a bind to the click function so it can correctly resolve this, in the constuctor of the rowEvent
My custom component has onChange event, which is working pretty good, but when I tried onSubmit, It does not work.
Alert does not display.
Currently my data provider get all values from inputs except my custom component, what should I do?
what's wrong with the code?
It's possible to pass data from this custom component to the parrent form?
Parrent form:
export const smthEdit = props => (
<Edit {...props} title={<smthTitle/>} aside={<Aside />}>
<SimpleForm>
<DisabledInput source="Id" />
<TextInput source="Name" />
<ReferrenceSelectBox label="Socket" source="SocketTypeId" reference="CPUSocketType"></ReferrenceSelectBox>
<DateField source="CreatedDate" showTime
locales={process.env.REACT_APP_LOCALE}
disabled={true} />
</SimpleForm>
</Edit>
);
My custom component (ReferrenceSelectBox):
handleSubmit(event) {
alert('smth');
}
render() {
return (
<div style={divStyle}>
<FormControl onSubmit={this.handleSubmit}>
<InputLabel htmlFor={this.props.label}>{this.props.label}</InputLabel>
<Select
multiple
style={inputStyle}
value={this.state.selectedValue}
onChange={this.handleChange}
>
{this.renderSelectOptions()}
</Select>
</FormControl>
</div>
);
}
Error is change FormControl to form
<form onSubmit={(event) => this.handleSubmit(event)}>
<InputLabel htmlFor={this.props.label}>{this.props.label}</InputLabel>
<Select
multiple
style={inputStyle}
value={this.state.selectedValue}
onChange={this.handleChange}
>
{this.renderSelectOptions()}
</Select>
</form>
Form Input.js
import React, { Component } from 'react';
import { Edit, SimpleForm, TextInput } from 'react-admin';
import SaveUpdate from './button/saveupdate.js';
export default class MemberDetail extends Component {
render(){
return (
<Edit title={"Member Detail"} {...this.props} >
<SimpleForm redirect={false} toolbar={<SaveUpdate changepage={changepage}>
<TextInput source="name" label="name"/>
</SimpleForm>
</Edit>)
}
}
/button/saveupdate.js
import React, { Component } from 'react';
import { Toolbar, UPDATE, showNotification, withDataProvider, GET_ONE} from 'react-admin';
import Button from '#material-ui/core/Button';
class SaveUpdate extends Component {
doSaveUpdate = (data) => {
const { dataProvider, dispatch } = this.props
dataProvider(UPDATE, endPoint, { id: data.id, data: { ...data, is_approved: true } })
.then((res) => {
dispatch(showNotification('Succes'));
})
.catch((e) => {
console.log(e)
dispatch(showNotification('Fail', 'warning'))
})
}
render(){
return (
<Toolbar {...this.props}>
<Button variant='contained' onClick={handleSubmit(data => { this.doSaveUpdate(data) })}>
SAVE
</Button>
</Toolbar>
)
}
export default withDataProvider(SaveUpdate);
This handlesubmit extra withDataProvider