How to handle Material UI inputs with React-hook-form? - reactjs

I'm working on a dynamic form where you can add and remove fields at will using React-hook-form. For this I made a table component where the input will be held. This is how it looks.
import {useFieldArray, useFormContext, Controller} from "react-hook-form";
import {cloneElement} from "react";
import {IoMdAdd, IoMdRemoveCircle} from "react-icons/io";
interface TableData {
tableName:string,
inputFields: {
title: string,
name: string,
inputComponent: React.ReactElement, // Did this since input can be text, select, or entire component
}[],
inputBlueprint: object,
min?: number
};
const InputTable = ({tableName, inputFields, inputBlueprint, min}: TableData) => {
const {fields, remove, append} = useFieldArray({name: tableName});
const {register, formState: {errors}, control} = useFormContext();
return (
<table className="table-auto border-collapse block m-auto w-fit max-w-xs max-h-48 overflow-auto sm:max-w-none my-3">
<thead className="text-center">
<tr>
{inputFields.map((input) => (
<td className="border-2 border-gray-400 px-5" key={input.title}>{input.title}</td>
))}
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.id}>
{inputFields.map((input) => (
<td key={input.title} className="border-gray-400 border-2 p-0">
{input.inputComponent.type === "input" && cloneElement(input.inputComponent, {
className: "bg-transparent outline-none block w-full focus:bg-gray-400 dark:focus:bg-gray-500 p-1",
...register(`${tableName}.${index}.${input.name}` as const)
})
}
{input.inputComponent.type !== "input" && //This doesn't work at all
<Controller
name={`${tableName}.${index}.${input.name}`}
control={control}
defaultValue=""
render={({field: {onChange, value}}) => {return input.inputComponent}}
/>
}
{errors[tableName]?.[index]?.[input.name] &&
<p className="bg-red-400 p-1">
{errors[tableName][index][input.name]?.message}
</p>
}
</td>
))}
{(min === undefined || min <= index) &&
<td onClick={() => remove(index)}><IoMdRemoveCircle className="text-red-600 text-2xl"/></td>
}
</tr>
))}
<tr>
<td onClick={() => append(inputBlueprint)} className="bg-green-500 border-gray-400 border-2"
colSpan={inputFields.length}>
<IoMdAdd className="m-auto"/>
</td>
</tr>
{errors[tableName] &&
<tr>
<td className="max-w-fit text-center">
{errors[tableName].message}
</td>
</tr>}
</tbody>
</table>
)
}
export default InputTable
It works for the most part with regular inputs (html input and select) but I'm having problems since I'm using Material's UI Autocomplete component for suggestions in some fields and since React-hook-form uses unregistered components and MUI uses registered, they really clash. Is there a better way to do this? I have thought about using the children prop but I'm not entirely sure if this would better the situation.

Related

Checkbox does not work with keys in React

I am writing a component where there are checkboxes. Using map I need to use keys in React. I created function that generates unique keys. But after that my checkboxes stop working properly. I cannot click some of them, some styles dissapear. Why is that going on? I thought keys are just such stuff needed for giving a unique id for React.
// generating id
export function uid(): string {
return (performance.now().toString(36) + Math.random().toString(16)).replace(/\./g, '');
}
// my component
export default function Table(props:ITable) {
if (props.loader) {
return <Loader />;
}
return (
<table className="table">
<thead>
<tr>
<th>
{props.actions.map((element) =>
element.onChangeAll &&
<Checkbox
key={uid()}
indeterminate
onChange={element.onChangeAll}
label={element.label}
disabled={props.columns.every((el) => el.disabled)}
/>)}
</th>
{props.headers.map((header) =>
<TableHeader
key={uid()}
header={header}
/>)}
{props.actions.map((element) =>
element.header &&
<th key={uid()}>{element.header}</th>)}
</tr>
</thead>
<tbody>
{props.columns.map((data) =>
<tr
className={classNames(
data.checked ? 'row-active' : 'row',
data.disabled && 'row-disabled'
)}
key={uid()}
>
<td>
{props.actions.map((element) =>
element.onChangeCheckbox &&
<Checkbox
onChange={element.onChangeCheckbox}
disabled={data.disabled}
checked={data.checked}
id={data.id.toString()}
key={uid()}
/>)}
</td>
{props.headers.map((header) =>
<TableRow
key={uid()}
columns={data}
header={header}
/>)}
{props.actions.map((element) =>
element.element &&
<td
key={uid()}
onClick={() => {
element.deleteRaw && element.deleteRaw(data.id);
element.openModal && element.openModal(data);
}}
>
{element.element}
</td>)}
</tr>
)}
</tbody>
</table>
);
}

Word is not getting added until the page is refreshed in TableItem component in React

TableItem component added without any data in UI. Could somebody help on this. On refereshing the UI, added data is shown with details in TableItem component.
Table Component Code
import TableItem from "./TableItem";
function Table({ searchWord }) {
const dispatch = useDispatch();
const dictData = useSelector((state) => state.dictionary);
useEffect(() => {
dispatch(getDictionaryAsync());
}, [dispatch]);
return (
<table className="table table-striped">
<thead>
<tr>
<th scope="col">Word</th>
<th scope="col">Description</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{dictData &&
dictData
.filter((e) =>
searchWord === ""
? e
: e.word &&
e.word.toLowerCase().includes(searchWord.toLowerCase())
)
.map((item) => (
<TableItem item={item} key={item.id} searchWord={searchWord} />
))}
</tbody>
</table>
);
}
export default Table;
Below is the TableItem Component Code which i am trying to update,
When i add a word to dictionary it will fetch the details from the server and display it in the React app.
function TableItem({ item }) {
const [modal, setModal] = useState(false);
const openModal = () => {
setModal(true);
};
return (
<>
<tr key={item.id}>
<td style={{ textTransform: "capitalize" }}>{item.word}</td>
<td>
<b style={{ textTransform: "capitalize" }}>
{item.items && item.items[0].category} -{" "}
</b>
{item.items && truncate(item.items[0].definitions[0])}
</td>
<td>
<button className="btn btn-danger btn-sm " onClick={openModal}>
View
</button>
</td>
</tr>
<Modal isOpen={modal} ariaHideApp={true}>
<div className="modal-header">
<h3 className="modal-word-header">
{item.word && item.word.toUpperCase()}
</h3>
<button
className="btn btn-danger btn-sm"
onClick={() => setModal(false)}
>
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
<div className="model-content">
<p>
{item.items &&
item.items.map((e) => {
return (
<>
<i>{e.category}</i>
<ul>
{e.definitions.map((def) => {
return <li>{def}</li>;
})}
</ul>
</>
);
})}
</p>
</div>
</Modal>
</>
);
}
Better add your TableItem component code!
Below code works fine and updated the UI on change in the Data in TableItem,
useEffect(() => {
dispatch(getDictionaryAsync());
}, [dispatch, dictData]); *<--updated code*

React Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead

I am new to react,
i am trying to use async function but i have facing the following error
"Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead."
import {
Badge,
Card,
CardHeader,
CardFooter,
DropdownMenu,
DropdownItem,
UncontrolledDropdown,
DropdownToggle,
Media,
Pagination,
PaginationItem,
PaginationLink,
Table,
Container,
Row,
UncontrolledTooltip,
} from "reactstrap";
// core components
import Header from "components/Headers/Header.js";
import 'react-toastify/dist/ReactToastify.css';
import { Link } from "react-router-dom";
import axios from "axios";
import data from "./data";
var apitoken= localStorage.getItem('apitoken');
const api=axios.create({baseURL:"https://IP/DeAPI/api/v1/account"})
const options = {
headers: {'Authorization': apitoken}
}
const asynchronousFunction = async () => {
const response = await api.get("/",options)
window.input=response.data.response
}
const mainFunction = async () => {
const result = await asynchronousFunction()
return result
}
const state = { students:window.input}
//console.log(window.input);
const renderTableData=async ()=> {
console.log("test"+await mainFunction())
if(state.students) {
return state.students.map((student, index) => {
const { id, name, age, email } = student //destructuring
return (
<tr key={id}>
<th scope="row">
<Media className="align-items-center">
<a
className="avatar rounded-circle mr-3"
href="#pablo"
onClick={(e) => e.preventDefault()}
>
<img
alt="..."
src={
require("../../assets/img/theme/bootstrap.jpg")
.default
}
/>
</a>
<Media>
<span className="mb-0 text-sm">{id} </span>
</Media>
</Media>
</th>
<td>
<Badge color="" className="badge-dot mr-4">{name}</Badge>
</td>
<td>
<Badge color="" className="badge-dot mr-4">{age}</Badge>
</td>
<td>
<Badge color="" className="badge-dot mr-4">{email}</Badge>
</td>
</tr>
)
})
}
else
{
console.log("Something went wrong")
}
}
const Accounts = () => {
return (
<>
<Header />
{/* Page content */}
<Container className="mt--7" fluid>
{/* Table */}
<Row>
<div className="col">
<Card className="shadow">
<CardHeader className="border-0">
<h3 className="mb-0">All Account</h3>
</CardHeader>
<div>
</div>
<Table className="align-items-center table-flush" responsive>
<thead className="thead-light">
<tr>
<th scope="col">Account Name</th>
<th scope="col">Phone</th>
<th scope="col">Email</th>
<th scope="col">Account Owner</th>
{/* <th scope="col">Pincode</th> */}
<th scope="col" />
</tr>
</thead>
<tbody>
{renderTableData()}
</tbody>
</Table>
</Card>
</div>
</Row>
</Container>
</>
);
};
export default Accounts;
What have i done wrong here;
I am trying to get the student data from api and render it in the component dynamically.
The issue is with
<tbody>
{renderTableData()}
</tbody>
If i removed it its working,
Thanks in advance
You should not put anything except JSX into JSX. The problem is that async function returns Promise instead of JSX. You should rather use React.useEffect and React.useState to solve the loading problem. Let me show you.
Acutally another problem might be returning an array. Try wrapping it into <>...</> as I did.
import * as React from "react";
import {
Badge,
Card,
CardHeader,
CardFooter,
DropdownMenu,
DropdownItem,
UncontrolledDropdown,
DropdownToggle,
Media,
Pagination,
PaginationItem,
PaginationLink,
Table,
Container,
Row,
UncontrolledTooltip,
} from "reactstrap";
// core components
import Header from "components/Headers/Header.js";
import 'react-toastify/dist/ReactToastify.css';
import { Link } from "react-router-dom";
import axios from "axios";
import data from "./data";
var apitoken= localStorage.getItem('apitoken');
const api=axios.create({baseURL:"https://IP/DeAPI/api/v1/account"})
const options = {
headers: {'Authorization': apitoken}
}
const TableData = () => {
const [students, setStudents] = React.useState([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(async () => {
const response = await api.get("/",options);
setStudents(response.data.response);
setLoading(false);
}, []);
if (loading) {
return <>Loading...</>;
}
return (
<>
{students.map((student, index) => {
const { id, name, age, email } = student //destructuring
return (
<tr key={id}>
<th scope="row">
<Media className="align-items-center">
<a
className="avatar rounded-circle mr-3"
href="#pablo"
onClick={(e) => e.preventDefault()}
>
<img
alt="..."
src={
require("../../assets/img/theme/bootstrap.jpg")
.default
}
/>
</a>
<Media>
<span className="mb-0 text-sm">{id} </span>
</Media>
</Media>
</th>
<td>
<Badge color="" className="badge-dot mr-4">{name}</Badge>
</td>
<td>
<Badge color="" className="badge-dot mr-4">{age}</Badge>
</td>
<td>
<Badge color="" className="badge-dot mr-4">{email}</Badge>
</td>
</tr>
)
})}
</>
)
}
const Accounts = () => {
return (
<>
<Header />
{/* Page content */}
<Container className="mt--7" fluid>
{/* Table */}
<Row>
<div className="col">
<Card className="shadow">
<CardHeader className="border-0">
<h3 className="mb-0">All Account</h3>
</CardHeader>
<div>
</div>
<Table className="align-items-center table-flush" responsive>
<thead className="thead-light">
<tr>
<th scope="col">Account Name</th>
<th scope="col">Phone</th>
<th scope="col">Email</th>
<th scope="col">Account Owner</th>
{/* <th scope="col">Pincode</th> */}
<th scope="col" />
</tr>
</thead>
<tbody>
<TableData />
</tbody>
</Table>
</Card>
</div>
</Row>
</Container>
</>
);
};
export default Accounts;
You should use the lifecycle hook useEffect to fetch async data (when working with functional components).
you also want to save your data in state. This is the default way to work with apis in react. Simply storing it in a variable will not work due to React's async behaviour.
Your error here is that you are trying to render a promise, which is not valid JSX
you can use the hook like this
const [state, setState] = useState({})
...
useEffect(() =>{
async(() =>{
const data = await mainFunction()
setState(data)
})()
}, []) //empty array means to only run once
<tbody>
{state.students?.map((student, index) => {
const { id, name, age, email } = student //destructuring
return (
<tr key={id}>
<th scope="row">
<Media className="align-items-center">
<a
className="avatar rounded-circle mr-3"
href="#pablo"
onClick={(e) => e.preventDefault()}
>
<img
alt="..."
src={
require("../../assets/img/theme/bootstrap.jpg")
.default
}
/>
</a>
<Media>
<span className="mb-0 text-sm">{id} </span>
</Media>
</Media>
</th>
<td>
<Badge color="" className="badge-dot mr-4">{name}</Badge>
</td>
<td>
<Badge color="" className="badge-dot mr-4">{age}</Badge>
</td>
<td>
<Badge color="" className="badge-dot mr-4">{email}</Badge>
</td>
</tr>
)
})
}
</tbody>
Try this one instead

How to circumvent material ui radio button limitation of being placed in a parent wrapping element?

I have the following react component:
import React, { Component } from 'react';
import classes from './generalinfo.css';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
class GeneralInfo extends Component {
_ToggleNextScreenButton = (e) => {
let currentState = this.props.infoObj;
let checkboxStatus = Object.keys(currentState).map( (value) => {
return currentState[value];
});
let ArroyOfCheckboxValues = checkboxStatus.filter((value) => {
return value === false;
});
if(ArroyOfCheckboxValues.length > 0) {
e.preventDefault();
}
}
render() {
return (
<div className={ classes.screen2 } >
<table className={ classes.initial__survey__details__table }>
<thead>
<tr>
<td>
Gender
</td>
<td>
Age
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="radio" name="genderRadio" value="male"
onChange={ (e) => { this.props.validateRadioInput({
name : e.target.getAttribute('name'),
value : e.target.getAttribute('value')
}) }
} />
<label>Male</label>
</td>
<td>
<input type="radio" name="ageRadio" value="Less than 35"
onChange={ (e) => { this.props.validateRadioInput({
name : e.target.getAttribute('name'),
value : e.target.getAttribute('value')
}) } } />
<label>Less than 35</label>
</td>
</tr>
<tr>
<td>
<input type="radio" name="genderRadio" value="Female"
onChange={ (e) => { this.props.validateRadioInput({
name : e.target.getAttribute('name'),
value : e.target.getAttribute('value')
}) } } />
<label>Female</label>
</td>
<td>
<input type="radio" name="ageRadio" value="More than 35"
onChange={ (e) => { this.props.validateRadioInput({
name : e.target.getAttribute('name'),
value : e.target.getAttribute('value')
}) } } />
<label>More than 35</label>
</td>
</tr>
<tr>
<td colSpan="2">
<Link to="/preferences" className={ [classes.btn , classes["btn--fullwidth"] , classes.btn__next ].join(' ') }
onClick={ (e) => this._ToggleNextScreenButton(e) } >
Next
</Link>
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
(unnecessary parts intentionally deleted).
As you can see i have independent radio elements inside a td , now i wanted to add radio element but using material-ui, the problem is all the radio buttons of a specific group have to be grouped under a parent <RadioButtonGroup /> as you can see below:
<RadioButtonGroup name="shipSpeed" defaultSelected="not_light">
<RadioButton
value="light"
label="Simple"
style={styles.radioButton}
/>
<RadioButton
value="not_light"
label="Selected by default"
style={styles.radioButton}
/>
<RadioButton
value="ludicrous"
label="Custom icon"
checkedIcon={<ActionFavorite style={{color: '#F44336'}} />}
uncheckedIcon={<ActionFavoriteBorder />}
style={styles.radioButton}
/>
</RadioButtonGroup>
How can i circumvent this limitation of the radio button being placed inside a parent wrapper (I.E. i'd like to use the radio button with a parent wrapper ) and still use material-ui ?
You can write a wrapper for RadioButton and then use that wrapper instead of the original RadioButton:
const TDRadioButton = ({wrapperProps, ...props}) => (
<td {...wrapperProps}>
<RadioButton {...props}>
</td>
);
This will wrap every radio button into a <td> while preserving all of its functionality by forwarding the props.

React: Proper way to update an array of objects in state

I am a professional web developer teaching myself react. I created this table as part of a larger form.
The table is invoked inside the form component
<ProductList
products={this.state.products}
onChange={products => this.sendUpdate('products', products)}
/>
this.sendUpdate:
sendUpdate(field, value) {
this.setState({[field]: value});
socket.emit('updateItem', this.state.id, {[field]: value});
}
That part is all working great with all my form updates. but now I am trying to figure out how to process the updates inside the table. Each product is a row of the table invoked like this:
<tbody>
{this.props.products.map((product, i) =>
<Product key={i} data={product} products={this}/>
)}
</tbody>
What is the proper way to update the state when I type in one of the inputs?
<FormControl
value={this.props.data.species}
onClick={e => this.updateProduct('species', e.target.value)}
/>
full code for ProductList
import React from "react";
import {Button, Table, FormControl} from "react-bootstrap";
class Product extends React.Component {
updateField(...props){
this.props.products.updateProduct(this.data, ...props)
}
render() {
return (
<tr>
<td>
<FormControl
value={this.props.data.species}
onClick={e => this.updateProduct('species', e.target.value)}
/>
</td>
<td><FormControl/></td>
<td><FormControl/></td>
<td><FormControl/></td>
<td><FormControl/></td>
<td><FormControl/></td>
<td><FormControl type="number"/></td>
<td><Button bsStyle="danger" onClick={() => this.props.products.deleteProduct(this.props.data)}>X</Button></td>
</tr>
);
}
}
export default class ProductList extends React.Component {
constructor(...props) {
super(...props);
}
addProduct() {
let products = this.props.products.concat([{timestamp: Date.now()}]);
this.props.onChange(products);
}
updateProduct(product, field, newValue) {
this.props.products;
// ???
}
deleteProduct(product) {
let products = this.props.products.filter(p => {
return p !== product
});
this.props.onChange(products);
}
render() {
return (
<Table responsive>
<thead>
<tr>
<th>Species</th>
<th>Dried</th>
<th>Cut</th>
<th>Dimensions Green</th>
<th>Dimensions Dry</th>
<th>Color</th>
<th>Quantity</th>
<th className="text-right">
<Button bsStyle="success" bsSize="xsmall" onClick={() => this.addProduct()}>Add</Button>
</th>
</tr>
</thead>
<tbody>
{this.props.products.map(product => <Product key={product.timestamp} data={product} products={this}/>)}
</tbody>
</Table>
);
}
}
This is what I ended up with based on the accepted answer:
import React from "react";
import {Button, Table, FormControl} from "react-bootstrap";
export default class ProductList extends React.Component {
constructor(...props) {
super(...props);
}
addProduct() {
let products = this.props.products.concat([{}]);
this.props.onChange(products);
}
updateProduct(product, field, newValue) {
const products = this.props.products.map(p => {
return p === product ? {...p, [field]: newValue} : p;
});
this.props.onChange(products);
}
deleteProduct(product) {
let products = this.props.products.filter(p => {
return p !== product
});
this.props.onChange(products);
}
render() {
return (
<Table responsive striped>
<thead>
<tr>
<th>Species</th>
<th>Dried</th>
<th>Cut</th>
<th>Dimensions Green</th>
<th>Dimensions Dry</th>
<th>Color</th>
<th>Quantity</th>
<th className="text-right">
<Button bsStyle="success" bsSize="xsmall" onClick={() => this.addProduct()}>Add</Button>
</th>
</tr>
</thead>
<tbody>
{this.props.products.map((product, i) => this.renderRow(i, product, this))}
</tbody>
</Table>
);
}
renderRow(i, product) {
return (
<tr key={i}>
<td>
<FormControl
value={product.species || ''}
onChange={e => this.updateProduct(product, 'species', e.target.value)}
/>
</td>
<td>
<FormControl
value={product.dried || ''}
onChange={e => this.updateProduct(product, 'dried', e.target.value)}
/>
</td>
<td>
<FormControl
value={product.cut || ''}
onChange={e => this.updateProduct(product, 'cut', e.target.value)}
/>
</td>
<td>
<FormControl
value={product.dimensionsGreen || ''}
onChange={e => this.updateProduct(product, 'dimensionsGreen', e.target.value)}
/>
</td>
<td>
<FormControl
value={product.dimensionsDry || ''}
onChange={e => this.updateProduct(product, 'dimensionsDry', e.target.value)}
/>
</td>
<td>
<FormControl
value={product.color || ''}
onChange={e => this.updateProduct(product, 'color', e.target.value)}
/>
</td>
<td>
<FormControl
type="number"
value={product.quantity || 0}
onChange={e => this.updateProduct(product, 'quantity', e.target.value)}
/>
</td>
<td><Button bsStyle="danger" onClick={() => this.deleteProduct(product)}>X</Button></td>
</tr>
);
}
}
In your ProductsList's render(), change the array map to something like:
{this.props.products.map((product, index) => <Product key={product.timestamp} data={product} index={index} products={this}/>)}
Then in your Product's change the updateField() to:
updateField(...props){
this.props.products.updateProduct(this.props.index, ...props)
}
And finally, change ProductsList's updateProduct() to:
updateProduct(index, field, newValue) {
const products = this.props.products.map((product, productIndex)) => {
if (index === productIndex) {
return {
...product,
[field]: newValue
};
}
return product;
})
this.props.onChange(products);
}
Also, there's a slight typo in Product render. The FormControl's onClick should read onClick={e => this.updateField('species', e.target.value)}.

Resources