Currently getting my head into React and DND for the first time. I have created a small example application where I try to implement a basic functionality of DND.
I´m running into two issues:
After I drop an item and reorder the state object (and set the state) the items get reseted to the inital state.
After the first drop I cannot drag all items further as they seem to not be associated with the draggable id anymore.
This screenshot shows that the source and destination id is different and also that the object order of the state has changed (onDragEnd function).
Also the second issue described is shown, that the draggable id is not available anymore.
Here is my code
import { PlusCircleOutlined } from "#ant-design/icons/lib/icons";
import { Row, Col, Button, Divider } from "antd";
import { useState } from "react";
import {
DragDropContext,
Draggable,
Droppable,
DroppableProvided,
DraggableLocation,
DropResult,
DroppableStateSnapshot,
DraggableProvided,
DraggableStateSnapshot,
} from "react-beautiful-dnd";
import Note from "../note/Note";
function Home() {
const [notes, setNotes] = useState([
{ title: "Hallo", text: "BlaBla" },
{ title: "Hallo1", text: "BlaBla1" },
{ title: "Hallo2", text: "BlaBla1" },
]);
function onDelete(index: number) {
const newNotes = [...notes];
newNotes.splice(index, 1);
setNotes(newNotes);
}
function onEdit(index: number, title: string, text: string) {
const newNotes = [...notes];
newNotes[index].title = title;
newNotes[index].text = text;
setNotes(newNotes);
}
function onCreateNote() {
let notesCount = notes.length;
setNotes((notes) => [
...notes,
{ title: "New" + notesCount, text: "New text" },
]);
}
function onDragEnd(result: DropResult): void {
console.log("DRAG END");
console.log(result);
const { source, destination } = result;
if (!destination) {
return;
}
console.log(notes);
const newNotes = [...notes];
const [removed] = newNotes.splice(source.index, 1);
newNotes.splice(destination.index, 0, removed);
console.log(newNotes);
setNotes(newNotes);
//Wenn mehr als eine liste
/* if (source.droppableId === destination.droppableId) {
return;
} */
}
return (
<div>
<Row>
<Col xs={2} sm={4} md={6} lg={6} xl={6}>
<Button
type="primary"
shape="round"
size="large"
icon={<PlusCircleOutlined />}
onClick={() => onCreateNote()}
>
Create new note
</Button>
</Col>
</Row>
<Divider></Divider>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
/* style={getListStyle(snapshot.isDraggingOver)} */
>
<Row gutter={[16, { xs: 8, sm: 16, md: 24, lg: 32 }]}>
{notes.map((value, index) => (
<Col
className="gutter-row"
xs={2}
sm={4}
md={6}
lg={6}
xl={6}
key={index}
>
<Draggable
key={index}
draggableId={value.title}
index={index}
>
{(providedDraggable: DraggableProvided) => (
<div
ref={providedDraggable.innerRef}
{...providedDraggable.draggableProps}
{...providedDraggable.dragHandleProps}
>
<Note
onDelete={() => onDelete(index)}
onEdit={(title: string, text: string) =>
onEdit(index, title, text)
}
noteTitle={value.title}
noteText={value.text}
/>
</div>
)}
</Draggable>
</Col>
))}
</Row>
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
}
export default Home;
Any idea what I´m missing here?
Related
I'm trying to create a payment method using nextJs library from stripe, like this:
import React, { FunctionComponent } from 'react';
import type { DisplaySettingsProps } from '#company/frontoffice/types';
import { Row, Col, Input } from '#company/ui';
import { useIntl } from '#company/frontoffice/providers';
import messages from './payment.lang';
import { loadStripe } from '#stripe/stripe-js';
import {
Elements,
useStripe,
useElements,
CardElement,
} from '#stripe/react-stripe-js';
/* Make sure to call `loadStripe` outside of a component’s render to avoid. recreating the `Stripe` object on every render. */
const stripePromise = loadStripe(
'pk_test_51JzJxAGAsaImrqUbu1pKDHiCSaq9A0sZTgpV6Li3sd8yYrCCVvPhCPfZ3i9JJ8ySgY5Uhceu6iGxlgylx2vuXBj100SpHqClA8'
);
const PaymentForm: FunctionComponent = () => {
const stripe = useStripe();
const elements = useElements();
const f = useIntl();
const handleSubmit = async (e) => {
e.preventDefault();
console.log('inside handleSubmit');
if (!stripe || !elements) {
console.log('!stripe || !elements');
return;
}
const cardElement = elements.getElement(CardElement);
/*
Returns:
result.paymentMethod: a PaymentMethod was created successfully.
result.error: there was an error.
*/
const { paymentMethod, error: backendError } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: 'Jenny Rosen',
},
});
if (backendError) {
console.error(backendError.message);
return;
}
};
return (
<Elements stripe={stripePromise}>
<form
onSubmit={(e) => {
console.log('handleSubmit');
handleSubmit(e);
}}
>
<Row mb={4}>
<Col col={12}>
<Input label={f(messages.name)} placeholder={f(messages.name)} required={true} />
</Col>
</Row>
<Row>
<Col>
<CardElement />
</Col>
</Row>
<Row>
<button>[submit /]</button>
</Row>
</form>
</Elements>
);
};
export type StripePaymentFormProps = DisplaySettingsProps & {
displaySettings: any /* TODO */;
};
const StripePaymentForm: FunctionComponent<StripePaymentFormProps> = () => {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
);
};
export default StripePaymentForm;
Which prints out this form:
But when I hit the submit button I see this error in the developer's console:
v3:1 Uncaught (in promise) IntegrationError: Invalid value for createPaymentMethod: card should be an object or element. You specified: null.
And If I console.log(cardElement) prints out null
Any idea why?
-EDIT-
I'm experiencing the same if I split the CardElement into CardNumber, CardExpiry and CardCVC (Which is actually the way I need it)
/* global fetch */
import React, { FunctionComponent } from 'react';
import type { DisplaySettingsProps } from '#company/frontoffice/types';
import { Row, Col, Text, Image, Input } from '#company/ui';
import { StyledInput } from './styledPayment';
import StripeInput from './Stripeinput';
import messages from './payment.lang';
import { useIntl } from '#company/frontoffice/providers';
import { loadStripe } from '#stripe/stripe-js';
import {
Elements,
useStripe,
useElements,
CardNumberElement,
CardCvcElement,
CardExpiryElement,
} from '#stripe/react-stripe-js';
/* Make sure to call `loadStripe` outside of a component’s render to avoid. recreating the `Stripe` object on every render. */
const stripePromise = loadStripe(
'pk_test_51JzJxAGAsaImrqUbu1pKDHiCSaq9A0sZTgpV6Li3sd8yYrCCVvPhCPfZ3i9JJ8ySgY5Uhceu6iGxlgylx2vuXBj100SpHqClA8'
);
const PaymentForm: FunctionComponent = () => {
const stripe = useStripe();
const elements = useElements();
const f = useIntl();
const handleSubmit = async (e) => {
e.preventDefault();
console.log('inside handleSubmit');
if (!stripe || !elements) {
console.log('!stripe || !elements');
return;
}
// what we had
const cardNumber = elements.getElement(CardNumberElement);
const cardCVC = elements.getElement(CardCvcElement);
const cardExpiry = elements.getElement(CardExpiryElement);
console.log(cardNumber, cardCVC, cardExpiry);
const { paymentMethod, error: backendError } = await stripe.createPaymentMethod({
type: 'card',
card: cardNumber,
billing_details: {
name: 'Jenny Rosen',
},
});
/* Create payment intent on server */
const { error: backendError_, clientSecret } = await fetch('/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentMethodType: 'card',
currency: 'eur',
}),
}).then((r) => r.json());
if (backendError) {
console.error(backendError.message);
return;
}
if (backendError_) {
console.error(backendError_.message);
return;
}
console.log(`clientSecret generated: ${clientSecret}`);
// Confirm de payment on the client
const { error: stripeError, paymentIntent, error } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: cardNumber,
},
}
);
if (stripeError) {
console.error(stripeError.message);
return;
}
console.log(`PaymentIntent ${paymentIntent.id} is ${paymentIntent.status}`);
// what we had
console.log(cardExpiry);
};
return (
<Elements stripe={stripePromise}>
<form
onSubmit={(e) => {
handleSubmit(e);
}}
>
<Row mb={4}>
<Col col={12}>
<Input label={f(messages.name)} placeholder={f(messages.name)} required={true} />
</Col>
</Row>
<Row mb={4}>
<Col col={12}>
<StyledInput
label={f(messages.number)}
placeholder={f(messages.number)}
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardNumberElement,
},
}}
/>
</Col>
</Row>
<Row colGap={5} mb={4}>
<Col col={[12, 6]}>
<StyledInput
label={f(messages.expiry)}
placeholder={f(messages.expiry)}
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardExpiryElement,
},
}}
/>
</Col>
<Col col={[10, 5]}>
<StyledInput
label={f(messages.security)}
placeholder="CVC"
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardCvcElement,
},
}}
/>
<Text as="p" fontSize="xs" color="text2" mt={2}>
{f(messages.securitySub)}
</Text>
</Col>
<Col col={[2, 1]}>
{/* Todo - replace the scr when we receive from design (already requested) */}
<Image
mt={[6, 6]}
display="block"
mx="auto"
width="size10"
maxWidth="sizeFull"
src="card-cvc.png"
alt="Credit card cvc icon"
/>
</Col>
</Row>
<Row>
<button>[submit /]</button>
</Row>
</form>
</Elements>
);
};
export type StripePaymentFormProps = DisplaySettingsProps & {
displaySettings: any /* TODO */;
};
const StripePaymentForm: FunctionComponent<StripePaymentFormProps> = () => {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
);
};
export default StripePaymentForm;
Providing this form:
Beeing all 3 console.log's null
And the stripeinput: (just to style them)
import React, { FunctionComponent, useRef, useImperativeHandle } from 'react';
type StripeInputPrpps = {
component: any; // todo type this
inputRef: any; // todo type this
props: any; // todo type this
};
const StripeInput: FunctionComponent<StripeInputPrpps> = ({
component: Component,
inputRef,
...props
}) => {
const elementRef = useRef();
/* Todo - obtain this values from theme.props */
const color = 'rgb(19, 40, 72)';
const placeholderColor = 'rgb(137, 147, 164)';
const inputStyle = {
color: color,
'::placeholder': {
color: placeholderColor,
},
};
useImperativeHandle(inputRef, () => ({
focus: () => {
if (elementRef && elementRef.current) {
elementRef.current.focus;
}
},
}));
return (
<Component
onReady={(element) => (elementRef.current = element)}
options={{
style: {
base: inputStyle,
},
}}
{...props}
/>
);
};
export default StripeInput;
Per #Jonatan-steele suggestion I deleted the duplicated Elements provider (the one in PaymentForm component) and it works just fine, the createPaymentMethod returns the paymentMethod as described in the docs.
return (
<form
onSubmit={(e) => {
handleSubmit(e);
}}
>
<Row mb={4}>
<Col col={12}>
<Input label={f(messages.name)} placeholder={f(messages.name)} required={true} />
</Col>
</Row>
<Row mb={4}>
<Col col={12}>
<StyledInput
label={f(messages.number)}
placeholder={f(messages.number)}
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardNumberElement,
},
}}
/>
</Col>
</Row>
<Row colGap={[0, 5]} mb={4}>
<Col col={[12, 6]}>
<StyledInput
label={f(messages.expiry)}
placeholder={f(messages.expiry)}
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardExpiryElement,
},
}}
/>
</Col>
<Col col={[10, 5]}>
<StyledInput
label={f(messages.security)}
placeholder="CVC"
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardCvcElement,
},
}}
/>
<Text as="p" fontSize="xs" color="text2" mt={2}>
{f(messages.securitySub)}
</Text>
</Col>
<Col col={[2, 1]}></Col>
</Row>
<Row>
<button>[submit /]</button>
</Row>
</form>
);
In my case, i had a loader replacing the payment component once payment button was pressed which disrupted the stripe flow! i removed the loader , then it worked fine.
2hrs wasted successfully
You should use parameter following below:
{
type: 'card'
card: elements.getElement(CardElement),
}
I have an array of items that i want to show with a map function, and every item is shown as a card.
I'm trying to show two kinds of cards with a different content, one if "isHover" is false, and the other if it true using onMouseEnter/onMouseOver.
I made "isHover" as an array in order to know which item to show/hide.
(The "isHover" array has the same length that the items' array has).
The problem is that when I hover one card it dissappears and nothing is shown in place of it. :(
The code:
function TeachersShow(props) {
const [isHover, setIsHover] = useState(null);
const updateIsHover = (index, isHover1) => {
let newArray = isHover;
newArray[index] = isHover1;
setIsHover([...newArray]);
console.log(isHover[index]);
};
return (
<div>
{isHover[index] === false && (<Card className="teacher-card"
onMouseEnter={() => { updateIsHover(index, true) }}
key={index}
item={item}
onClick={() => navigateToTeacher(item)}
>
<Card.Img className="teachersImg" src={item.photoURL}>
</Card.Img>
<Card.Title className=" teachersName">
{item.username}
</Card.Title>
</Card>)}
{isHover[index] === true && (
<Card className="card-hover"
onMouseleave={() => { updateIsHover(index, false) }}
key={index}
item={item}
onClick={() => navigateToTeacher(item)}
>
<Card.Title className=" teachersName">
{item.username}
</Card.Title>
<Card.Subtitle className="proTeacher">
{`${item.profession} teacher`}
</Card.Subtitle>
<Card.Text className="teacherDesc">
{item.teacher_description}
</Card.Text>
</Card>)}
</Col>
))}
<Col></Col>
</Row>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TeachersShow);
It's little hard to tell what's wrong, since a lot of information is missing from the code. But try to create a component Let's say <Teacher />, and let it be responsible for hovering action. Try this:
function Teacher(item) {
const [hover, setHover] = useState(false);
const renderCardData = () => {
if (!hover) {
return (
<Card.Img className="teachersImg" src={item.photoURL} />
<Card.Title className="teachersName">
{item.username}
</Card.Title>
);
}
return (
<Card.Title className=" teachersName">
{ item.username }
</Card.Title>
<Card.Subtitle className="proTeacher">
{ `${item.profession} teacher` }
</Card.Subtitle>
<Card.Text className="teacherDesc">
{ item.teacher_description }
</Card.Text>
);
};
return (
<Card
className={ hover
? 'card-hover'
: 'teacher-card' }
onMouseEnter={ () => setHover(true) }
onMouseLeave={ () => setHover(false) }
>
{ renderCardData() }
</Card>
);
}
export default Teacher;
And you render it like that:
function TeachersList(teachers) {
return teachers.map(Teacher);
};
I'm receiving the error 'expected an assignment of function call and instead saw an expression.
*The above error is resolved, it's now giving me an Unhandled Rejection (TypeError): render is not a function. It functions properly until I go to the products page and then gives me the error.
I console logged it and it was pulling the information but then it breaks when I import the ProductBrowse component to display the information.
ProductPage:
class ProductPage extends React.Component {
state = {
shoppingCartOpen: false,
};
drawerToggleClickHandler = () => {
this.setState((prevState) => {
return { shoppingCartOpen: !prevState.shoppingCartOpen };
});
};
render() {
let shoppingCartDrawer;
if (this.state.shoppingCartOpen) {
shoppingCartDrawer = <ShoppingCartDrawer />;
}
return (
<ProductStyling>
<ButtonAppBar drawerClickHandler={this.drawerToggleClickHandler} />
<H1>Products</H1>
{shoppingCartDrawer}
<ProductConsumer>
<Grid container spacing={3}>
{(value) => {
return value.products.map((prod, idx) => {
return (
<Grid item xs={3} key={`${prod.name}-${prod.store}-${idx}`}>
<ProductBrowse
productName={prod.name}
productDesc={prod.desc}
productImg={prod.img}
productPrice={prod.price}
/>
</Grid>
);
});
}}
</Grid>
</ProductConsumer>
;
</ProductStyling>
);
}
}
Structure of value:
const ProductContext = React.createContext();
class ProductProvider extends React.Component {
state = {
products: productData,
};
addToCart = () => {
console.log('Hello from add to cart'); };
render() {
return (
<ProductContext.Provider
value={{ ...this.state, addToCart: this.addToCart }}
>
{this.props.children}
</ProductContext.Provider>
);
}
}
const ProductConsumer = ProductContext.Consumer;
export { ProductProvider, ProductConsumer };
ProductBrowse:
const ProductBrowse = ({
productName,
productDesc,
productImg,
productPrice,
}) => {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const openModal = () => {
setOpen(!open);
};
const closeModal = () => {
setOpen(!open);
};
return (
<Box border={1} borderRadius={3}>
<Card className={classes.root}>
<CardActionArea onClick={() => openModal()}>
<ProductModal
open={open}
onClick={() => openModal()}
onClose={() => closeModal()}
onSave={() => closeModal()}
productName={productName}
productDesc={productDesc}
/>
<CardHeader
title={productName}
subheader={formatCurrency(productPrice)}
/>
<CardMedia
className={classes.media}
image={productImg}
alt={productDesc}
/>
<CardContent>
<Typography variant='body2' color='textSecondary' component='p'>
{productDesc}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button size='small' /*To Checkout*/>BUY NOW</Button>
<Button size='small'>ADD TO CART</Button>
<Button size='small'>REVIEW</Button>
</CardActions>
</Card>
</Box>
);
};
ProductData:
import desk from '../../../assets/img/desk.webp';
export const productData = [
{
img: desk,
name: 'Desk',
store: 'Local Furniture Shop 1',
price: 9.99,
desc: "This sturdy desk is built to outlast years of coffee and hard work. You get a generous work surface and a clever solution to keep cords in place underneath."
},
Index.js:
ReactDOM.render(
<React.StrictMode>
<Auth0Provider
domain={auth0Domain}
client_id={auth0ClientID}
redirect_uri={window.location.origin}
onRedirectCallback={onRedirectCallback}
audience={auth0Audience}
scope={"read:current_user"}
>
<ProductProvider>
<Provider store={store}>
<App />
</Provider>
</ProductProvider>
</Auth0Provider>
</React.StrictMode>,
document.getElementById('root')
);
You are not returning anything from ProductConsumer You need to do like this:
<ProductConsumer>
<Grid container spacing={3}>
{(value) => {
return value.products.map((prod, idx) => {
return (
<Grid item xs={3} key={`${prod.name}-${prod.store}-${idx}`}>
<ProductBrowse
productName={prod.name}
productDesc={prod.desc}
productImg={prod.img}
productPrice={prod.price}
/>
</Grid>
);
});
}}
</Grid>
</ProductConsumer>;
In my Reactjs app, I need to have a parent component (a wizard) named Wizard.js and a number of child components (steps of the wizard) named PrimaryForm.js, SecondaryForm.js etc. They all are Class based components with some local validation functions.
Previous and Next buttons to advance the steps, reside in the Wizard.js.
To advance the next step of the wizard, I'm trying to call a method from PrimaryForm. I checked similar questions in Stackoverflow; tried using ref or forwardRef, but I could not make it work. I currently receive "TypeError: Cannot read property 'handleCheckServer' of null" error.
Below are my parent and child classes. Any help about what I would be doing wrong is appreciated.
Wizard.js:
import React, { Component } from 'react';
...
const getSteps = () => {
return [
'Info',
'Source Details',
'Target Details',
'Configuration'
];
}
class Wizard extends Component {
constructor(props) {
super(props);
this.firstRef = React.createRef();
this.handleNext = this.handleNext.bind(this);
this.state = {
activeStep: 1,
}
}
componentDidMount() {}
handleNext = () => {
if (this.state.activeStep === 1) {
this.firstRef.current.handleCheckServer(); <<<<<<<<<<<<<<<<< This is where I try to call child method
}
this.setState(state => ({
activeStep: state.activeStep + 1,
}));
};
handleBack = () => {
this.setState(state => ({
activeStep: state.activeStep - 1,
}));
};
handleReset = () => {
this.setState({
activeStep: 0,
});
};
render() {
const steps = getSteps();
const currentPath = this.props.location.pathname;
const { classes } = this.props;
return (
<React.Fragment>
<CssBaseline />
<Topbar currentPath={currentPath} />
<div className={classes.root}>
<Grid container spacing={2} justify="center" direction="row">
<Grid container spacing={2} className={classes.grid} justify="center" direction="row">
<Grid item xs={12}>
<div className={classes.topBar}>
<div className={classes.block}>
<Typography variant="h6" gutterBottom>Wizard</Typography>
<Typography variant="body1">Follow the wizard steps to create a configuration.</Typography>
</div>
</div>
</Grid>
</Grid>
<Grid container spacing={2} alignItems="center" justify="center" className={classes.grid}>
<Grid item xs={12}>
<div className={classes.stepContainer}>
<div className={classes.bigContainer}>
<Stepper classes={{ root: classes.stepper }} activeStep={this.state.activeStep} alternativeLabel>
{steps.map(label => {
return (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
);
})}
</Stepper>
</div>
<PrimaryForm ref={this.firstRef} />
</div>
</Grid>
</Grid>
<Grid container spacing={2} className={classes.grid}>
<Grid item xs={12}>
<div className={classes.flexBar}>
<Tooltip title="Back to previous step">
<div>
<Button variant="contained"
disabled={(this.state.activeStep === 0)}
className={classes.actionButton}
onClick={this.handleBack}
size='large'>
<BackIcon className={classes.rightIcon} />Back
</Button>
</div>
</Tooltip>
<Tooltip title="Proceed the next step">
<div>
<Button
variant="contained" className={classes.actionButton}
color="primary"
size='large'
disabled={!(!this.state.isFormValid || this.state.isTestWaiting)}
onClick={this.handleNext}>
<ForwardIcon className={this.props.classes.rightIcon}/>Next</Button>
</div>
</Tooltip>
<Tooltip title="Cancel creating new configuration">
<Button variant="contained" color="default" className={classes.actionButton}
component={Link} to={'/configs'} style={{ marginLeft: 'auto' }}>
<CancelIcon className={classes.rightIcon} />Cancel
</Button>
</Tooltip>
</div>
</Grid>
</Grid>
</Grid>
</div>
</React.Fragment>
)
}
}
export default withRouter(withStyles(styles)(Wizard));
PrimaryForm.js:
import React, { Component } from 'react';
...
class PrimaryForm extends Component {
constructor(props) {
super(props);
this.handleCheckServer = this.handleCheckServer.bind(this);
this.state = {
hostname: {
value: "localhost",
isError: false,
errorText: "",
},
serverIp: {
value: "127.0.0.1",
isError: false,
errorText: "",
},
isFormValid: true,
isTestValid: true,
testErrorMessage: "",
isTestWaiting: false,
};
}
componentDidMount() { }
handleCheckServer() {
alert('Alert from Child. Server check will be done here');
}
evaluateFormValid = (prevState) => {
return ((prevState.hostname.value !== "" && !prevState.hostname.isError) &&
(prevState.serverIp.value !== "" && !prevState.serverIp.isError));
};
handleChange = event => {
var valResult;
switch (event.target.id) {
case 'hostname':
valResult = PrimaryFormValidator.validateHostname(event.target.value, event.target.labels[0].textContent);
this.setState({
...this.state,
hostname:
{
value: event.target.value,
isError: valResult.isError,
errorText: valResult.errorText,
},
});
break;
case 'serverIp':
valResult = PrimaryFormValidator.validateIpAddress(event.target.value, event.target.labels[0].textContent);
this.setState({
...this.state,
serverIp:
{
value: event.target.value,
isError: valResult.isError,
errorText: valResult.errorText,
}
});
break;
default:
}
this.setState(prevState => ({
...prevState,
isFormValid: this.evaluateFormValid(prevState),
}));
}
render() {
const { classes } = this.props;
return (
<React.Fragment>
<div className={classes.bigContainer}>
<Paper className={classes.paper}>
<div>
<div>
<Typography variant="subtitle1" gutterBottom className={classes.subtitle1} color='secondary'>
Primary System
</Typography>
<Typography variant="body1" gutterBottom>
Information related with the primary system.
</Typography>
</div>
<div className={classes.bigContainer}>
<form className={classes.formArea}>
<TextField className={classes.formControl}
id="hostname"
label="FQDN Hostname *"
onChange={this.handleChange}
value={this.state.hostname.value}
error={this.state.hostname.isError}
helperText={this.state.hostname.errorText}
variant="outlined" autoComplete="off" />
<TextField className={classes.formControl}
id="serverIp"
label="Server Ip Address *"
onChange={this.handleChange}
value={this.state.serverIp.value}
error={this.state.serverIp.isError}
helperText={this.state.serverIp.errorText}
variant="outlined" autoComplete="off" />
</form>
</div>
</div>
</Paper>
</div>
</React.Fragment>
)
}
}
export default withRouter(withStyles(styles)(PrimaryForm));
(ps: I would like to solve this without another framework like Redux, etc if possible)
Example in Typescript.
The idea is that the parent passes its callback to the child. The child calls the parent's callback supplying its own e.g. child callback as the argument. The parent stores what it got (child callback) in a class member variable and calls it later.
import * as React from 'react'
interface ICallback {
(num: number): string
}
type ChildProps = {
parent_callback: (f: ICallback) => void;
}
class Child extends React.Component {
constructor(props: ChildProps) {
super(props);
props.parent_callback(this.childCallback);
}
childCallback: ICallback = (num: number) => {
if (num == 5) return "hello";
return "bye";
}
render() {
return (
<>
<div>Child</div>
</>
)
}
}
class Parent extends React.Component {
readonly state = { msg: "<not yet set>" };
letChildRegisterItsCallback = (fun: ICallback) => {
this.m_ChildCallback = fun;
}
callChildCallback() {
const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
console.log("Child callback returned string: " + str);
return str;
}
componentDidMount() {
this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
}
render() {
return (
<>
<Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
<div>{this.state.msg}</div>
</>
)
}
m_ChildCallback: ICallback | undefined = undefined;
}
P.S.
The same code in Javascript. The only difference is that interface, type, readonly and type annotations are taken out. Pasting into here confirms it's a valid ES2015 stage-2 code.
class Child extends React.Component {
constructor(props) {
super(props);
props.parent_callback(this.childCallback);
}
childCallback = (num) => {
if (num == 5) return "hello";
return "bye";
}
render() {
return (
<>
<div>Child</div>
</>
)
}
}
class Parent extends React.Component {
state = { msg: "<not yet set>" };
letChildRegisterItsCallback = (fun) => {
this.m_ChildCallback = fun;
}
callChildCallback() {
const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
console.log("Child callback returned string: " + str);
return str;
}
componentDidMount() {
this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
}
render() {
return (
<>
<Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
<div>{this.state.msg}</div>
</>
)
}
m_ChildCallback = undefined;
}
I am using 'react-form-validator-core' package and trying to create a custom form validator that implements 'mui-downshift', a Material UI implementation of PayPal's downshift. This question is mostly about 'react-form-validator-core' package itself. The problem is that the form itself does not register the validator component I've created. Here is my full code of the custom component and the form itself. I've exhausted my debugging skills, but what I noticed is that there's something wrong with the this.context in the form...
Validator component:
import React from 'react';
import PropTypes from 'prop-types';
import MuiDownshift from 'mui-downshift';
import { ValidatorComponent } from 'react-form-validator-core';
class AutocompleteValidator extends ValidatorComponent {
constructor(props) {
debugger;
super(props);
this.originalItems = props.items.map(({key, name}) => ({ text: name, value: key }));
this.handleStateChange = this.handleStateChange.bind(this);
this.errorText = this.errorText.bind(this);
}
componentWillMount() {
if (!this.filteredItems) {
this.setState({filteredItems: this.originalItems});
}
if (!!this.props.value) {
const selectedItem = this.originalItems.filter(
item => item.value.toLowerCase().includes(this.props.value.toLowerCase())
)[0];
this.setState({ selectedItem })
} else {
this.setState({ selectedItem: null})
}
}
componentWillReceiveProps(nextProps) {
// If no filteredItems in sate, get the whole list:
if (!nextProps.value) {
this.setState({ isValid: false })
}
}
handleStateChange(changes) {
// If searching
if (changes.hasOwnProperty('inputValue')) {
const filteredItems = this.originalItems.filter(
item => item.text.toLowerCase().includes(changes.inputValue.toLowerCase())
);
this.setState({ filteredItems })
}
// If something is selected
if (changes.hasOwnProperty('selectedItem')) {
!!changes.selectedItem ? this.setState({isValid: true}) : this.setState({isValid: false})
// If we get undefined, change to '' as a fallback to default state
changes.selectedItem = changes.selectedItem ? changes.selectedItem : '';
this.props.onStateChange(changes);
}
}
errorText() {
const { isValid } = this.state;
if (isValid) {
return null;
}
return (
<div style={{ color: 'red' }}>
{this.getErrorMessage()}
</div>
);
}
render() {
return (
<div>
<MuiDownshift
{...this.props}
items={this.state.filteredItems}
onStateChange={this.handleStateChange}
ref={(r) => { this.input = r; }}
defaultSelectedItem={this.state.selectedItem}
/>
{this.errorText()}
</div>
);
}
}
AutocompleteValidator.childContextTypes = {
form: PropTypes.object
};
export default AutocompleteValidator;
A component where it's used:
render() {
return (
<ValidatorForm
ref='form'
onSubmit={() => {
this.context.router.history.push(this.props.config.urls['step5']);
}}
onError={errors => console.log(errors)}
>
<Row>
<Col md={12}>
<AutocompleteValidator
validators={['required']}
errorMessages={['Cette information doit être renseignée']}
isRequired={true}
name='bankId'
items={this.props.config.list.bank}
onStateChange={(changes) => {
this.props.loansUpdate('bankId', changes.selectedItem.value);
}}
value={!!this.props.loans.bankId ? this.props.loans.bankId : false}
/>
</Col>
</Row>
<Row>
<Col md={12} style={{ marginTop: '15px' }}>
<Checkbox
label={<Translate value='step4.insuranceProvidedByBank' />}
labelStyle={{ 'top': '0px' }}
name='insuranceProvidedByBank'
value={this.props.loans.insuranceProvidedByBank}
checked={this.props.loans.insuranceProvidedByBank}
onCheck={(event, value) => {
this.props.loansUpdate('insuranceProvidedByBank', value);
}}
/>
</Col>
</Row>
<Row>
<Col sm={6} md={5}>
<Button
fullWidth
style={{ marginTop: '50px', marginBottom: '20px' }}
type='submit'
icon={<i className='fa fa-angle-right' style={{ marginLeft: '10px', display: 'inline-block' }} />}
><Translate value='iContinue' /></Button>
</Col>
</Row>
</ValidatorForm >
)
};
};
you get this problem because you override default componentWillMount and componentWillReceiveProps methods from ValidatorComponent. So, to solve this case you should do something like this
componentWillMount() {
// should call parent method before
super.componentWillMount();
// your code
}