I want to save additional data for the custom node, I used this article for creating a custom node using additional data.
Custom node:
import { useState } from 'react';
import { Handle, Position } from 'react-flow-renderer';
import { Modal } from "react-bootstrap";
import { Form } from 'react-bootstrap';
import { triggerLimitationType } from "../../../enums/triggerLimitationType";
import _ from 'lodash';
import i18next from "../../../i18n";
function CustomNode({ data }) {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const defaultValues = { type: 0 };
const [formData, setFormData] = useState(defaultValues);
const handleChange = event => {
const name = event.target.name;
const value = event.target.value;
setFormData(values => ({ ...values, [name]: value }))
data.attribute = formData;
}
return (
<row>
<div onDoubleClick={handleShow}>
<Handle type="target" position={Position.Top} />
<div className="node-icon" >
<i className="feather icon-skip-back"></i>
</div>
<Handle type="source" position={Position.Bottom} id="b" />
</div>
{show && (
<Modal show={show} onHide={handleClose} className="right fade">
<Modal.Header closeButton>
<Modal.Title>{i18next.t("Trigger")} : {i18next.t("VisitorReturn")} </Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group >
<Form.Label>Type</Form.Label>
<Form.Control required as="select" placeholder="Enter Name" value={formData.type} name="type" onChange={handleChange} >
{_.map(triggerLimitationType, (value, key) => (
<option value={value} key={value}>{key}</option>
))}
</Form.Control>
<Form.Control.Feedback type="invalid">Please choose a type.</Form.Control.Feedback>
</Form.Group>
</Modal.Body>
</Modal>
)}
</row>
);
}
export default CustomNode;
Created node:
const onDrop = (event) => {
event.preventDefault();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const type = event.dataTransfer.getData("type");
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top
});
console.log("event", event.dataTransfer);
const newNode = {
id: getId(),
position,
type: type,
data: {
label: `${type} node`, attribute: {
type: 0
} }
};
// setElements((es) => es.concat(newNode));
setElements([...elements, newNode]);
// onSave();
dispatch(updateElements(newNode));
console.log("ONDROP", elements);
};
But when I set the value for an attribute, I get the error: "attribute" is read-only.
Related
I have been working on refactoring code for a crypto trading platform - changing the class based components to functional components.
The main aim is to display every info of selected coin pair of binance.com using websocket.
If coin pair is changed by selecting option, we have to close old socket and create new socket connection, at first the plan was to implement this in TradeCoinSelection.tsx, but there is a part that is getting coin pair list and calling function of 'parent' (index.tsx) with changed info.
What is happening is the socket thread is not killed, even though there is close() function, the close() function is not finding the old socket thread because ws is resetting in re-rendering.
This means the front end seems to be alternating between new and old pair data as the old pair socket thread is not closed properly.
Any help is appreciated.
Functional component code - TradeCoinSection.tsx
import { useState, useEffect } from 'react';
import Select, { Option } from 'rc-select';
import { Row, Col, Card } from 'reactstrap';
import TradeApi from '#api/TradeApi';
import { IExchangeListAPIResponse } from '#api/types/exchange';
import { connect } from 'react-redux';
import { AppDispatch, RootState } from '#redux/store';
import { fetchExchangeCoinListAction } from '#redux/actions/Exchange';
import { ICoinSymbolResponse } from '#api/types/trade';
import ReconnectingWebSocket from 'reconnecting-websocket';
function TradeCoinSection (props: any) {
const [disabled] = useState<boolean>(false);
const [symbolLoader, setSymbolLoader] = useState<boolean>(false);
const [symbolList, setSymbolList] = useState<Array<ICoinSymbolResponse>>(props.exchangeCoinList);
const [binanceTickerData, setBinanceTickerData] = useState<Record<string, string>>({
h: "",
c: "",
b: "",
p: "",
l: "",
v: "",
q: "",
});
const [didLoadSymbolList] = useState<boolean>(false);
let ws : ReconnectingWebSocket = new ReconnectingWebSocket('wss://stream.binance.com:9443/ws/bnbusdt#ticker');
ws.close();
const closeAndOpenTickerStream = (symbol: string) => {
if(symbol != "") {
ws.close()
ws = new ReconnectingWebSocket('wss://stream.binance.com:9443/ws/'+symbol.toLowerCase()+'#ticker');
ws.addEventListener('open', () => {
// console.log('connected');
});
// eslint-disable-next-line #typescript-eslint/no-explicit-any
ws.onmessage = (evt: any) => {
// listen to data sent from the websocket server
const message = JSON.parse(evt.data);
setBinanceTickerData(message);
};
ws.addEventListener('close', () => {
// console.log('disconnected');
});
}
};
// eslint-disable-next-line #typescript-eslint/no-explicit-any
const onSymbolChangeHandler = (value: string, option: any) => {
let baseAssest = '';
let quoteAssest = '';
if (value) {
const splitArray = option.key.split('-');
if (splitArray.length > 1) {
baseAssest = splitArray[0];
quoteAssest = splitArray[1];
}
}
console.log("1,", value);
props.symbolChangeHandler(value, quoteAssest, baseAssest);
closeAndOpenTickerStream(value);
};
const fetchCoinPairList = async () => {
try {
setSymbolLoader(true);
// setErrorList([]);
console.log("extra-1")
if(props.selectedExchangeAccountSite != '') {
const response = await TradeApi.getCoinPair(props.selectedExchangeAccountSite);
// const response = await TradeApi.getCoinPair();
setSymbolList(response.data);
props.fetchExchangeCoinListAction(response.data);
let symbolObj = response.data[0];
if (props.selectedSymbol) {
response.data.find((obj) => {
if (obj.symbol === props.selectedSymbol) {
symbolObj = obj;
}
});
}
onSymbolChangeHandler(symbolObj.symbol, {
key: symbolObj.symbol_pair,
value: symbolObj.symbol,
});
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
} catch (err: any) {
} finally {
setSymbolLoader(false);
console.log(symbolLoader)
}
};
useEffect(() => {
if (props.exchangeCoinList && props.exchangeCoinList.length < 1) {
fetchCoinPairList();
}
return () => {
ws.close();
}
}, []);
useEffect(() => {
if(props.selectedExchangeAccountSite !== '' && !didLoadSymbolList) {
fetchCoinPairList();
}
if (
props.exchangeCoinList &&
props.exchangeCoinList.length > 0 &&
!props.selectedSymbol_state
) {
setSymbolList(props.exchangeCoinList);
let symbolObj = props.exchangeCoinList[0];
if (props.selectedSymbol) {
props.exchangeCoinList.find((obj: any) => {
if (obj.symbol === props.selectedSymbol) {
symbolObj = obj;
}
});
}
onSymbolChangeHandler(symbolObj.symbol, {
key: symbolObj.symbol_pair,
value: symbolObj.symbol,
});
}
}, [props.selectedExchangeAccount]);
const temp_disabled = disabled;
return (
<Card className="shadow-xl p-3">
<Row className="less-gutters align-items-start">
<Col xl={2} lg={12} md="12">
<Row className="less-gutters flex-column flex-sm-row flex-xl-column">
{!props.dropdownDisabled && (
<Col sm={6} md={6} lg={6} xl={12}>
<Select
placeholder="Select account"
style={{ width: '100%' }}
value={props.selectedExchangeAccount}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
onChange={(exchangeId: number, options: any) =>
props.exchangeAccountChangeHandler(exchangeId, options.label, options.site)
}
className="mb-2"
>
{props.exchangeList.length > 0
? props.exchangeList.map((obj: IExchangeListAPIResponse) => (
<Option key={obj.id} value={obj.id} label={obj.account_name} site={obj.exchange}>
<b>{obj.account_name}</b>
</Option>
))
: null}
</Select>
</Col>
)}
<Col sm={6} md={6} lg={6} xl={12}>
<Select
showSearch={true}
disabled={
props.dropdownDisabled ? props.dropdownDisabled : temp_disabled
}
placeholder="Select Symbol"
style={{ width: '100%' }}
value={props.selectedSymbol}
onChange={onSymbolChangeHandler}
className="currency-dropdown"
>
{symbolList.length > 0
? symbolList.map((obj: ICoinSymbolResponse) => (
<Option value={obj.symbol} key={obj.symbol_pair}>
<b>{obj.symbol_pair}</b>
</Option>
))
: null}
</Select>
</Col>
</Row>
</Col>
<Col xl={10} lg={12} md="12">
<div className="d-flex w-100 market-data-container mt-md-3 mt-lg-3 mt-xl-0">
<div className="w-100 d-flex align-items-start mt-3 mt-md-0 sm:flex-col">
<div className="last-price-block col-md-4 col-lg-3 pl-0 pl-md-0 pl-lg-3 pr-3">
<div className="market-data-last-price">
{binanceTickerData.h &&
parseFloat(binanceTickerData.c).toFixed(8)}
</div>
<div className="market-data-worth">
{binanceTickerData.b &&
`${parseFloat(binanceTickerData.b).toFixed(8)}`}
</div>
</div>
<div className="market-data d-flex flex-wrap w-100 justify-content-start pl-0 pl-md-3 mt-3 mt-md-0">
<div className="market-data-block w-33 mb-md-3 mb-lg-2">
<div className="market-data-block-title">24h Change</div>
<div className="market-data-block-value">
{binanceTickerData.p &&
parseFloat(binanceTickerData.p).toFixed(2)}{' '}
{binanceTickerData.p}%
</div>
</div>
<div className="market-data-block w-33 mb-md-3 mb-lg-2">
<div className="market-data-block-title">24h High</div>
<div className="market-data-block-value">
{binanceTickerData.h &&
parseFloat(binanceTickerData.h).toFixed(8)}
</div>
</div>
<div className="market-data-block w-33 mb-md-3 mb-lg-2">
<div className="market-data-block-title">24h Low</div>
<div className="market-data-block-value">
{binanceTickerData.l &&
parseFloat(binanceTickerData.l).toFixed(8)}
</div>
</div>
<div className="market-data-block w-33 mb-md-3 mb-lg-0">
<div className="market-data-block-title">
24h Volume({props.quoteAssest})
</div>
<div className="market-data-block-value">
{binanceTickerData.v &&
parseFloat(binanceTickerData.v).toFixed(2)}
</div>
</div>
<div className="market-data-block w-33 mb-md-0 mb-lg-0">
<div className="market-data-block-title">
24h Volume({props.baseAssest})
</div>
<div className="market-data-block-value">
{binanceTickerData.q &&
parseFloat(binanceTickerData.q).toFixed(2)}
</div>
</div>
</div>
</div>
</div>
</Col>
</Row>
</Card>
);
}
const mapStateToProps = (state: RootState, props: any) => ({
exchangeCoinList: state.exchange.exchangeCoinList,
ownProps: props
});
const mapDispatchToProps = (dispatch: AppDispatch) => ({
fetchExchangeCoinListAction: (data: Array<ICoinSymbolResponse>) =>
dispatch(fetchExchangeCoinListAction(data)),
});
// export default TradeCoinSection;
export default connect(mapStateToProps, mapDispatchToProps)(TradeCoinSection);
Parent (Index.tsx)
import { useState, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { Row, Col } from 'reactstrap';
import { BarChart2 } from 'react-feather';
import { connect } from 'react-redux';
// Import Api and constants;
import TradeApi from '#api/TradeApi';
import AppConfig from '#config/config';
import { getCatchError } from '#helpers/ErrorHandler';
import { RootState } from '#redux/store';
import TradeCoinSection from '#pages/Admin/Trade/Trade/partials/TradeCoinSection';
import TradeChartSection from '#pages/Admin/Trade/Trade/partials/TradeChartSection';
import OrderHistorySection from '#pages/Admin/Trade/OrderHistory/partials/OrderHistorySection';
import OpenOrderSection from '#pages/Admin/Trade/OpenOrder/partials/OpenOrderSection';
import PageTitle from '#components/ui/PageTitle/PageTitle';
import TradeAddSection from '#pages/Admin/Trade/Trade/partials/TradeAddSection';
import EmptyDataComponent from '#components/EmptyDataComponent';
import LoaderComponent from '#components/LoaderComponent';
import ExchangeModal from '../../Settings/partials/ExchangeModal';
function TradePage (props: any) {
const [ symbolPriceObj, setSymbolPriceObj ] = useState({
symbol: '',
price: '',
status: '',
filter_data: null,
symbol_info: null,
} as any);
const [symbolDetailLoader, setSymbolDetailLoader] = useState<boolean>(false);
const [isAddExchangeModalOpen, setIsAddExchangeModalOpen] = useState<boolean>(false);
const [selectedExchangeAccount, setSelectedExchangeAccount] = useState<number>(0);
const [selectedExchangeAccountLabel, setSelectedExchangeAccountLabel] = useState<string>('');
const [selectedExchangeAccountSite, setSelectedExchangeAccountSite] = useState<string>('');
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
const [baseAssest, setBaseAssest] = useState<string>('');
const [quoteAssest, setQuoteAssest] = useState<string>('');
const [errorList, setErrorList] = useState<Array<string>>([]);
const [lastRecordInsertedAt, setLastRecordInsertedAt] = useState<number>(0);
const postOrderHandler = () => {
setLastRecordInsertedAt(Date.now());
}
const addExchangeModalHandler = (isAdded?: boolean) => {
setIsAddExchangeModalOpen(!isAddExchangeModalOpen);
if (isAdded) {
setTimeout(() => {
window.location.reload();
}, 2000);
}
}
const exchangeAccountChangeHandler = (exchangeId: number, exchangeLabel: string, exchangeSite: string) => {
setSelectedExchangeAccount(exchangeId);
setSelectedExchangeAccountLabel(exchangeLabel);
setSelectedExchangeAccountSite(exchangeSite);
}
const symbolChangeHandler = (symbol: string, quoteAssest: string, baseAssest: string) => {
setSelectedSymbol(symbol);
setQuoteAssest(quoteAssest);
setBaseAssest(baseAssest);
}
const fetchSymbolDetailHandler = async () => {
try {
setSymbolDetailLoader(true);
setErrorList([]);
const response = await TradeApi.getCoinPairPrice(selectedExchangeAccount, selectedSymbol);
const coinObj = {
filter_data: response.data.filter_data,
symbol_info: response.data.symbol_info,
price: response.data.price,
symbol: '',
status: '',
};
setSymbolPriceObj(coinObj);
} catch (error: any) {
console.log(symbolDetailLoader, errorList);
setSymbolDetailLoader(false);
setErrorList(getCatchError(error));
} finally {
setSymbolDetailLoader(false);
}
}
useEffect(() => {
if(selectedSymbol != '')
fetchSymbolDetailHandler();
}, [selectedSymbol])
useEffect(() => {
document.title = `${AppConfig.REACT_APP_NAME} | Trade`;
if (props.exchangeList && props.exchangeList.length > 0) {
const exchangeList = props.exchangeList;
setSelectedExchangeAccount(exchangeList[0].id);
setSelectedExchangeAccountLabel(exchangeList[0].account_name);
setSelectedExchangeAccountSite(exchangeList[0].exchange);
postOrderHandler();
}
}, []);
useEffect(() => {
if (props.exchangeList.length > 0) {
const exchangeList = props.exchangeList;
setSelectedExchangeAccount(exchangeList[0].id);
setSelectedExchangeAccountLabel(exchangeList[0].account_name);
setSelectedExchangeAccountSite(exchangeList[0].exchange);
postOrderHandler();
}
}, [props]);
useEffect(() => {
postOrderHandler();
}, [selectedExchangeAccount])
return (
<>
<PageTitle
titleHeading="Trade"
titleDescription="Buy and sell all of your currencies"
pageTitleIconBox={true}
pageTitleIcon={<BarChart2 className="display-2 text-primary" />}
/>
{props.exchangeLoader && <LoaderComponent isSmallLoader={true} />}
{!props.exchangeLoader &&
props.exchangeList &&
props.exchangeList.length > 0 && (
<>
<Row className="less-gutters buysell">
<Col xl={3} lg={4} md={5} className="mb-4 mb-md-0 pl-2 pr-2">
<TradeAddSection
selectedSymbol={selectedSymbol}
selectedExchangeAccount={selectedExchangeAccount}
selectedExchangeSite={selectedExchangeAccountSite}
symbolPriceObj={symbolPriceObj}
quoteAssest={quoteAssest}
baseAssest={baseAssest}
selectedAccountLabel={selectedExchangeAccountLabel}
postOrderHandler={postOrderHandler}
/>
</Col>
<Col xl={9} lg={8} md={7}>
<TradeCoinSection
selectedSymbol={selectedSymbol}
exchangeList={props.exchangeList}
selectedExchangeAccount={selectedExchangeAccount}
selectedExchangeAccountSite={selectedExchangeAccountSite}
exchangeAccountChangeHandler={exchangeAccountChangeHandler}
symbolChangeHandler={symbolChangeHandler}
quoteAssest={quoteAssest}
baseAssest={baseAssest}
/>
{selectedSymbol && (
<TradeChartSection selectedSymbol={selectedSymbol} />
)}
</Col>
</Row>
{selectedExchangeAccount && (
<OpenOrderSection
selectedExchangeAccount={selectedExchangeAccount}
lastRecordInsertedAt={lastRecordInsertedAt}
selectedSymbol={selectedSymbol}
isTradePage={true}
/>
)}
{selectedExchangeAccount && (
<OrderHistorySection
selectedExchangeAccount={selectedExchangeAccount}
lastRecordInsertedAt={lastRecordInsertedAt}
selectedSymbol={selectedSymbol}
isTradePage={true}
/>
)}
</>
)}
{!props.exchangeLoader &&
props.exchangeList &&
props.exchangeList.length < 1 && (
<Row className="less-gutters buysell">
<Col xl={12} lg={12} md={12}>
<EmptyDataComponent
message="No exchange connected. Please connect an exchange first"
buttonText="Connect Exchange"
buttonClickHandler={addExchangeModalHandler}
showExchangeLinkSection={true}
/>
<ExchangeModal
isOpen={isAddExchangeModalOpen}
toggler={addExchangeModalHandler}
/>
</Col>
</Row>
)}
</>
);
}
const mapStateToProps = (state: RootState, props: any) => ({
exchangeList: state.exchange.exchangeList,
exchangeLoader: state.exchange.exchangeListStatusObj.loading,
ownProps: props
});
export default withRouter(connect(mapStateToProps, {})(TradePage));
I've imported useAsync(hook from 'react-async') and I'm trying to use it after the client submits the form to send a post request.
Now, I'm getting an error that I can't use hooks inside functions based on the rules of hooks.
how can solve it? so that I'll be able to use useAsync after the client submits the form.
handleSubmit is my onSubmit function.
Here's my code :
import { Modal } from 'react-bootstrap';
import "react-datepicker/dist/react-datepicker.css";
import DatePicker from 'react-datepicker'
import { useState } from 'react';
import { useAsync } from 'react-async';
import useFetch from '../../hooks/useFetch'
import { useLocation } from 'react-router-dom';
const TodoPopup = (props : {show : boolean, onHide : () => void, title : string, values?
: {title : string, expirationDate : any, description : string}}) => {
const [name, setName] = useState(props.values?.title || '');
const [date, setDate] = useState(props.values?.expirationDate || '');
const [description, setDescription] = useState(props.values?.description || '');
const url = useLocation().pathname;
const handleSubmit = (e : React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// const fetchRelevant = useFetch.apply(this,
// (props.values? [url, 'PUT', {name, date, description}] : [url, 'POST',{name,date, description}]));
const fetchRelevant = useFetch.apply(this, [url, props.values? 'PUT' : 'POST',
{name, date, description}]);
const { data, error, isPending} = useAsync({promiseFn : () => {
return fetchRelevant;
}})
if(isPending) console.log('loading...');
if(error) console.log('error');
if(data) props.onHide();
}
return (
<Modal {...props} centered>
<Modal.Header closeButton>
<Modal.Title>{props.title}</Modal.Title>
</Modal.Header>
<form onSubmit={(e) => handleSubmit(e)}>
<Modal.Body>
<div className = "form-group">
<label>Task Name</label>
<input type="text" className = "form-control" value = {name} onChange = {(e) =>
setName(e.target.value)} required />
</div>
<div className = "form-group">
<label>Expired</label>
<DatePicker selected={date} onChange={e => setDate(e)} className="form-control"
minDate={new Date()} required />
</div>
<div className = "form-group">
<label>Description</label>
<textarea rows = {5} className = "form-control" value = {description} onChange =
{(e) => setDescription(e.target.value)}></textarea>
</div>
</Modal.Body>
<Modal.Footer>
<button className="btn btn-primary" type="submit">Save changes</button>
</Modal.Footer>
</form>
</Modal>
);
};
export default TodoPopup;
*Note - I've tried to name the onSubmit function with an uppercase letter but it caused a runtime error.
There is a function exposed by useFetch called run , you can call it like so :
import React, { useState } from "react"
import { useFetch } from "react-async"
const TodoPopup = (props) => {
const { isPending, error, run } = useFetch("URL", { method: "POST" })
const [name, setName] = useState(props.values?.title || '');
const [date, setDate] = useState(props.values?.expirationDate || '');
const [description, setDescription] = useState(props.values?.description || '');
const handleSubmit = e => {
e.preventDefault()
run({ body: JSON.stringify({name, date, description}) })
}
return (
<form onSubmit={handleSubmit}>
...
</form>
)
}
export default TodoPopup;
I am implementing a simple signup page with React Typescript.
I'm trying to set the gender with the radio button, save it in the state, and send it to the server, but the toggle doesn't work.
What should I do?
//RegisterPage.tsx
const [radioState, setradioState] = useState(null);
const [toggle, settoggle] = useState<boolean>(false);
const onRadioChange = (e: any) => {
setradioState(e);
console.log(radioState);
};
const genderOps: ops[] = [
{ view: "man", value: "man" },
{ view: "woman", value: "woman" },
];
<div>
{genderOps.map(({ title, gender }: any) => {
return (
<>
<input
type="radio"
value={gender}
name={gender}
checked={gender === radioState}
onChange={(e) => onRadioChange(gender)}
/>
{title}
</>
);
})}
</div>
You should do some changes on your code, here what you should do:
import React, { EventHandler, useState } from "react";
import "./styles.css";
export default function App() {
const [radioState, setradioState] = useState("");
const [toggle, settoggle] = useState<boolean>(false);
const onRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setradioState(e.currentTarget.value);
};
const genderOps = [
{ view: "man", value: "man" },
{ view: "woman", value: "woman" }
];
return (
<div className="App">
<div>
{genderOps.map(({ view: title, value: gender }: any) => {
return (
<>
<input
type="radio"
value={gender}
name={gender}
checked={gender === radioState}
onChange={(e) => onRadioChange(e)}
/>
{title}
</>
);
})}
</div>{" "}
</div>
);
}
In my simple crud application, when I try to add a new author with an invalid name format and try to submit form display an error and after that when I have press backspace twice to erase last letter in textbox.
Here is my AuthorForm.tsx
import React, {useEffect, useState} from 'react';
import {Row, Col, Form, Button} from 'react-bootstrap';
import {IAuthor} from "../../assets/types/LibraryTypes";
import {XCircle} from "react-feather";
import {useForm} from "react-hook-form";
interface IFormInputs {
authorName: string
}
type CreateFormProps = {
onClose: () => void,
onAuthorAdded: (author:IAuthor)=>void,
onAuthorToUpdate: IAuthor | null,
updateAuthorIndex : number | null,
onAuthorUpdated : (updatedAuthor:IAuthor, index:number)=>void,
}
const AuthorForm:React.FC<CreateFormProps> = (props) =>{
const [authorName, setAuthorName] = useState<string|null>(null);
const { register, errors, handleSubmit } = useForm<IFormInputs>({mode:'onTouched'});
useEffect(()=>{
if(!props.onAuthorToUpdate){
setAuthorName(null);
return;
}
setAuthorName(props.onAuthorToUpdate.name);
},[props.onAuthorToUpdate]);
const handleOnNameChange = (event:React.ChangeEvent<HTMLInputElement>)=>{
event.preventDefault();
setAuthorName(event.target.value);
}
const handleOnCreate = () =>{
if(!authorName){
return;
}
if(props.onAuthorToUpdate && props.updateAuthorIndex !== null){
props.onAuthorUpdated({...props.onAuthorToUpdate,name:authorName},props.updateAuthorIndex);
setAuthorName(null);
return;
}
const newAuthor: IAuthor = {name:authorName};
props.onAuthorAdded(newAuthor);
setAuthorName(null);
};
return(
<Col className='p-0' sm={10}>
<Row className=' pb-1 mb-3 mx-1'>
<Col xs={10}>
<span className='add-book-title pt-2'>
{!props.onAuthorToUpdate && 'Create Author'}
{props.onAuthorToUpdate && 'Update Author'}
</span>
</Col>
<Col className='closeBtn text-right p-0' xs={2}>
<XCircle color='#363636' className='mt-2 mr-3' onClick={props.onClose}/>
</Col>
</Row>
<Form className='mx-4' onSubmit={handleSubmit(handleOnCreate)}>
<Form.Group>
<Form.Row>
<Form.Label column="sm" xs={6} className='label'>
Name of the Author
</Form.Label>
<Col xs={6} className='warning text-right mt-2 pr-2'>
{errors.authorName?.type === "required" && (
<p>This field is required</p>
)}
{errors.authorName?.type === "maxLength" && (
<p>Author Name name cannot exceed 50 characters</p>
)}
{errors.authorName?.type === "pattern" && (
<p>Invalid Author Name</p>
)}
</Col>
<Col sm={12}>
<Form.Control size={"sm"}
name="authorName"
ref={register({
required: true,
maxLength: 50,
pattern: /^[a-zA-Z\s]+$/
})}
onChange={
(event:React.ChangeEvent<HTMLInputElement>)=>
handleOnNameChange(event)
}
value={authorName?authorName:''}
/>
</Col>
</Form.Row>
</Form.Group>
<Col className='text-right mb-3 p-0' xs={12}>
<Button type={"submit"} variant={"primary"} size={"sm"} className={"px-3 pt-1"}>
{!props.onAuthorToUpdate && 'Create'}
{props.onAuthorToUpdate && 'Update'}
</Button>
</Col>
</Form>
</Col>
)
};
export default AuthorForm;
And this is AuthorList.tsx
import React, {useEffect, useState} from 'react';
import {Container} from 'react-bootstrap';
import AuthorAddedList from "./AuthorAddedList";
import AuthorForm from "./AuthorForm";
import AuthorWelcome from "./AuthorWelcome";
import CreateAuthor from "./CreateAuthor";
import {IAuthor} from "../../assets/types/LibraryTypes";
const AuthorList:React.FC = () =>{
const initAuthors: IAuthor[] = [];
const [authors, setAuthors] = useState<IAuthor[]>(initAuthors);
const [isFormVisible, setIsFormVisible] = useState<boolean>(false);
const [authorToUpdate, setAuthorToUpdate] = useState<IAuthor | null>(null);
const [updateAuthorIndex, setUpdateAuthorIndex] = useState<number| null>(null)
useEffect(()=>{
if(!authorToUpdate){
return;
}
setIsFormVisible(true);
},[authorToUpdate]);
const handleOnCreateClick = () => {
setIsFormVisible(true);
setAuthorToUpdate(null);
};
const handleOnFormClosed = () => {
setIsFormVisible(false);
}
const handleAuthorAdded = (newAuthor: IAuthor) => {
const allAuthors: IAuthor[] = authors.slice();
allAuthors.push(newAuthor)
setAuthors(allAuthors);
};
const handleAuthorDeleted = (index: number) => {
const allAuthors: IAuthor[] = authors.slice();
allAuthors.splice(index, 1);
setAuthors(allAuthors);
}
const handleOnUpdateRequest = (index: number) => {
setAuthorToUpdate(authors[index]);
setUpdateAuthorIndex(index);
setIsFormVisible(true);
}
const handleOnAuthorUpdated = (updatedAuthor: IAuthor, index:number) =>{
const allAuthors : IAuthor [] = authors.slice();
allAuthors.splice(index,1, updatedAuthor);
setAuthors(allAuthors)
}
return (
<Container fluid={true} className={"authors"}>
<AuthorWelcome/>
<AuthorAddedList authors={authors} onDeleted={handleAuthorDeleted} onUpdateRequested={handleOnUpdateRequest} />
<CreateAuthor onClickCreate={handleOnCreateClick}/>
{isFormVisible &&
<AuthorForm onClose={handleOnFormClosed} onAuthorAdded={handleAuthorAdded} onAuthorToUpdate={authorToUpdate} onAuthorUpdated={handleOnAuthorUpdated} updateAuthorIndex={updateAuthorIndex}/>}
</Container>
)
}
export default AuthorList;
Here is the sandbox link to my full code Click Here
to demonstrate the error,
go to the sandbox link
click add author button in webapp
enter an invalid name like 'john97'
then submit the form
then try to clear the name using backspace.
now you can see to erase last letter 'j' you have to press backspace twice
please help me to solve this issue
thank you
I think using Controller component of react-hook-form is better at handling controlled components, you also don't have to set onChange event and make your code much cleaner.
Using this in AuthorForm.tsx seems to make your weird bug fixed.
type FormData = {
authorName: string;
}
//some codes...
const { register, handleSubmit, control, errors, setValue, reset } = useForm<FormData>();
//some codes...
const handleOnCreate = (data: FormData) => {
if (!data?.authorName) {
return;
}
if (props.onAuthorToUpdate && props.updateAuthorIndex !== null) {
props.onAuthorUpdated(
{ ...props.onAuthorToUpdate, name: data.authorName },
props.updateAuthorIndex
);
reset({ authorName: "" }); // or setValue("authorName", "");
return;
}
const newAuthor: IAuthor = { name: data.authorName };
props.onAuthorAdded(newAuthor);
reset({ authorName: "" }); // or setValue("authorName", "");
};
//some codes...
<Controller
control={control}
name={"authorName"}
as={<Form.Control size={"sm"} />}
defaultValue=""
rules={{
required: true,
maxLength: 50,
pattern: /^[A-Za-z ]+$/i
}}
/>
Here is the sandbox.
the problem is in the AuthorForm.tsx file, in this line:
<Form className='mx-4' onSubmit={handleSubmit(handleOnCreate)}>
the onSubmit shouldn't accept an invoked function handleSubmit(handleOnCreate),
it should be changed to onSubmit={() => handleSubmit(handleOnCreate)}
Hello everyone :D I need your advise/tip. Right now I have a APIDataTable component. It has its rows, columns and etc. This component is responsible to show/build data table on frontend with search bar in it above the table. I have an search bar, which is not functional right now. I want it to search data from data table. What should I start from? How can i make it perform search in Table. Thank you for any advise and tip <3
import React, { useEffect, useState } from "react";
import { plainToClassFromExist } from "class-transformer";
import { Pagination } from "../../models/Pagination";
import {
DataTable,
DataTableHead,
DataTableHeadCell,
DataTableBody,
DataTableRow,
DataTableCell,
} from "../DataTable";
import { request } from "../../api";
import "./index.css";
import { MenuSurface } from "../MenuSurface";
import { IconButton } from "../IconButton";
import { Checkbox } from "../Checkbox";
import { Dialog } from "../Dialog";
import { GridCell, GridRow } from "../Grid";
import { Button } from "../Button";
export class Column<T> {
label: string;
width?: number;
filter?: JSX.Element;
render: (row: T) => JSX.Element | string;
constructor(column: Partial<Column<T>>) {
Object.assign(this, column);
}
}
type APIDataTableProps<T> = {
apiPath?: string;
params?: string;
columns?: Column<T>[];
type: Function;
onRowClick?: (row: T) => void;
};
export const APIDataTable = <T extends object>({
apiPath,
params,
columns,
type,
onRowClick,
}: APIDataTableProps<T>) => {
const [data, setData] = useState<Pagination<T>>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(15);
const [isLoading, setIsLoading] = useState(false);
const [isDialogOpen, setDialogOpen] = useState(false);
const [isMenuSurFaceOpen, setMenuSurfaceOpen] = useState(false);
const [hiddenColumns, setHiddenColumns] = useState<number[]>(
JSON.parse(localStorage.getItem(`hiddenColumns-${apiPath + params}`)) || []
);
const fetchData = async () => {
const urlSearchParams = new URLSearchParams(params);
urlSearchParams.set("page", page.toString());
urlSearchParams.set("page_size", pageSize.toString());
const url = `${apiPath}?${urlSearchParams}`;
const response = await request(url);
const data = plainToClassFromExist(new Pagination<T>(type), response, {
excludeExtraneousValues: true,
});
setData(data);
setIsLoading(false);
};
useEffect(() => {
if (!!apiPath) {
setIsLoading(true);
fetchData();
}
}, [page, pageSize]);
const headCells = columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => (
<DataTableHeadCell key={column.label} width={column.width}>
{column.label}
</DataTableHeadCell>
));
const rows = data?.results?.map((row, index) => (
<DataTableRow
key={"row-" + index}
onClick={() => !!onRowClick && onRowClick(row)}
>
{columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => {
return (
<DataTableCell key={column.label} width={column.width}>
<div className="data-table-cell-text">{column.render(row)}</div>
</DataTableCell>
);
})}
</DataTableRow>
));
let uncheckedCheckboxes = hiddenColumns;
const onCheckboxChange = (index: number, value: boolean) => {
if (!value) {
uncheckedCheckboxes.push(index);
//setHiddenColumns(uncheckedCheckboxes);
} else {
const array = [...uncheckedCheckboxes];
array.splice(array.indexOf(index), 1);
uncheckedCheckboxes = array;
}
};
const [isOpen, setIsOpen] = useState(false);
return (
<div className="data-table-container">
<div className="search-test">
<div className="mdc-menu-surface--anchor">
<label
className="mdc-text-field mdc-text-field--filled mdc-text-field--no-label mdc-text-field--with-leading-icon mdc-text-field--with-trailing-icon"
htmlFor="input"
id="search-menu-surface"
>
<IconButton density={-1} icon="search" />
<input
className="mdc-text-field__input "
type="text"
placeholder="Поиск"
id="searchinput"
/>
<IconButton
density={-1}
icon="arrow_drop_down"
onClick={() => {
setMenuSurfaceOpen(true);
}}
/>
</label>
<MenuSurface
isOpen={isMenuSurFaceOpen}
onClose={() => setMenuSurfaceOpen(false)}
fullwidth
>
<div className="data-table-filters-container">
{columns.map(
(column) =>
!!column.filter && (
<div className="data-table-filter">
<div className="data-table-filter-label mdc-typography--subtitle1">
{column.label}
</div>
<div className="data-table-column-filter">
{column.filter}
</div>
</div>
// <GridRow>
// <GridCell span={3}>{column.label}</GridCell>
// <GridCell span={3}>{column.filter}</GridCell>
// </GridRow>
)
)}
{/* <GridCell span={2}> */}
{/* <Button label="Поиск" raised /> */}
{/* <Button
label="Отмена"
raised
onClick={() => {
setIsOpen(false);
}}
/> */}
{/* </GridCell> */}
</div>
</MenuSurface>
</div>
<IconButton
onClick={() => {
setDialogOpen(true);
}}
density={-1}
icon="settings"
/>
<Dialog
isOpen={isDialogOpen}
onOkClick={() => {
localStorage.setItem(
`hiddenColumns-${apiPath + params}`,
JSON.stringify(uncheckedCheckboxes)
);
setDialogOpen(false);
setHiddenColumns(uncheckedCheckboxes);
}}
onCloseClick={() => setDialogOpen(false)}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{columns.map((column, index) => (
<Checkbox
label={column.label}
onChange={(value) => onCheckboxChange(index, value)}
defaultChecked={!uncheckedCheckboxes.includes(index)}
/>
))}
</div>
</Dialog>
</div>
<DataTable
pagination={true}
count={data?.count}
rowsNumber={data?.results.length}
page={page}
next={data?.next}
previous={data?.previous}
isLoading={isLoading}
onNextClick={() => setPage(page + 1)}
onPreviosClick={() => setPage(page - 1)}
>
<DataTableHead>{headCells}</DataTableHead>
<DataTableBody>{rows}</DataTableBody>
</DataTable>
</div>
);
};
I'm guessing that you want to search bar to effectively filter out rows that don't match. in this case what you want to do is add a filter to the search text (naturally you'll add a state for the search value, but it looks like you'll have that handled.
You'll add your filter here const rows = data?.results?.filter(...).map
You filter function will look something like this
const rows = data?.results.filter((row) => {
// In my own code if I have other filters I just make them return false
// if they don't match
if (
searchText &&
!(
// exact match example
row.field === searchText ||
// case-insensitive example
row.otherField?.toLowerCase().includes(searchText)
// can continue with '||' and matching any other field you want to search by
)
)
return false;
return true;
}).map(...)