Trying to write a test for my react-hook-form just so I can check if it has rendered properly. At the moment I'm failing at the first hurdle and can't get my form to render its children:
const props = {
request: true,
title: "testing title",
commentPlaceholder: "Placeholder",
tip: "helpful tip",
submitButtonText: "submit",
onSubmit: jest.fn(),
recipientName: "User one",
activeStep: 2,
};
it('Should render the CommentForm as if it was in the request flow', async () => {
const { getByText, getByTestId, debug } = render(
<CommentForm {...props} />
);
console.log(debug());
});
The console log outputs the below:
<body>
<div>
<form />
</div>
</body>
Where as the component I'm testing has lots of children in it to create the form.
I've pinned pointed it to the wrapper that I am using in the component.
<StyledForm onSubmit={onSubmit} schema={CommentSchema} onChange={handleChange}>
If I change this to form then it renders all my children. This is a styled component that extends this method below:
const Form = ({ className, onSubmit, schema, defaultValues, children, mode, onChange, style }) => {
const methods = useForm({
defaultValues,
validationSchema: schema,
validateCriteriaMode: 'all',
mode: mode || 'onSubmit',
});
const { handleSubmit, watch } = methods;
const values = watch();
useEffect(() => {
onChange && onChange(values);
}, [onChange, values]);
return (
<FormContext {...methods}>
<form style={style} className={className} onSubmit={handleSubmit(onSubmit)}>
{children}
</form>
</FormContext>
);
};
I know that this is causing the problem but I can't understand why it wouldn't render the components. This is also the reason why when I change the StyledForm to a normal form element it works.
** UPDATE **
Now found out that it seems to be because I'm extending the styledComponent like so:
export const StyledForm = styled(Form)`
width: 100%;
margin: 0 auto 50px;
`;
jest.mock('hoc/withForm', () => jest.fn(({ children }) => <form>{children}</form>));
This was the solution. I had to mock my HOC so that it returned a more basic element.
Related
I have a children component as a shared form using with different parent components, those parent components have different buttons and their actions.
My parent component is like:
const form = useForm({
defaultValues: {
name: '',
}
})
const { handleSubmit } = form
const onSubmit = (data) => { ... }
return (
<>
<Child form={form} />
<button onClick={handleSubmit(onsubmit)} />
</>
)
The child component is:
const {register, control } = props.form
return (
<form>
<Controller control={control} name="name" render={(field, fieldState) => <input .../>} />
</form>
)
How can I test the children component input behavior?
I used a jest.mock to react-hook-form:
jest.mock('react-hook-form', () => {
...jest.requireActural('react-hook-form')
Controller: ()=><></>
useForm:()=>({
Control: ()=>({}),
handleSubmit: ()=>jest.fn()
})
})
But this mock cannot get the input on the DOM. On the screen.debug, I can only see a tag renders, but not the content.
https://codesandbox.io/s/react-hook-form-v7-controller-forked-fwjqxg?file=/src/index.js
i am using react hook form to create a from with multiple pages
i am able to create it and it working with all filed except file-input-type how do i pass i file from another page and finaly pass it to api in the final page
i a have actualy 3 pages i have only added the 1st and final page (fist page has the file input filed and final page has the api to which it must be submitted)
form with file upload field
import { useForm } from "react-hook-form";
export default function Form(props) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
<input style={styles.file} type="file" />
</div>
<input {...register("name", { required: true })}
name="husband"
value={props.getState("name")}
onChange={props.handleChange}
style={styles.input}
type="text"
placeholder="Name"
/>
<input onClick={handleSubmit(props.next)}
type="submit"
value="Next"
/>
form with submit button and api to which it must be uploaded
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const submitValue = (e) => {
// e.preventDefault();
props.state.patient = "true";
const data = props.state;
axios
.post("registration/", data)
.then(() => {
alert("updated data");
window.location = "/clogin";
})
.catch((error) => {
//var my_obj_str = JSON.stringify(error.response.data);
alert(JSON.stringify(error.response.data));
});
};
codesandbox
https://codesandbox.io/s/wizardly-worker-zicnr?file=/src/App.js
There are 2 options
Single form wraps all the steps
You can wrap the <Steps /> component with one form. Make the <Step />s components stateless that accepts onInputChange which will called upon input changes.
onInputChange call setValue to update the form's state.
When the form submitted, you have the file (among other inputs) so you can send it to the server.
import { useEffect } from "react";
import { Steps, StepsProvider, useSteps } from "react-step-builder";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, setValue } = useForm();
useEffect(() => {
register("myFile");
}, [register]);
const onInputChange = (e) => {
setValue(e.target.name, e.target.files[0]);
};
const onSubmit = (data) => {
alert(`Your file name: ${data.myFile.name}, size: ${data.myFile.size}`);
};
return (
<StepsProvider>
<form onSubmit={handleSubmit(onSubmit)}>
<MySteps onInputChange={onInputChange} />
</form>
</StepsProvider>
);
}
const MySteps = ({ onInputChange }) => {
const { next, prev } = useSteps();
return (
<Steps>
<div>
<h1>Step 1</h1>
<input type="file" name="myFile" onChange={onInputChange} />
<button onClick={next}>Next</button>
</div>
<div>
<h1>Step 2</h1>
<button>Submit</button>
</div>
</Steps>
);
};
https://codesandbox.io/s/gifted-wozniak-of14l?file=/src/App.js
Multiple forms in each step
If you want need to have a form inside each step, you can pass the step's data up to the parent when upon step's form submission. Still the parent has the form state so it can handle when all the steps completed
import { useRef } from "react";
import { Steps, StepsProvider, useSteps } from "react-step-builder";
import { useForm } from "react-hook-form";
export default function App() {
const formState = useRef();
const onStepComplete = (data) => {
formState.current = {
...formState.current,
...data
};
};
const onComplete = (data) => {
onStepComplete(data);
const {
name,
myFile: [file]
} = formState.current;
alert(
`Your name: ${name} Your file name: ${file.name}, size: ${file.size}`
);
};
return (
<StepsProvider>
<MySteps onStepComplete={onStepComplete} onComplete={onComplete} />
</StepsProvider>
);
}
const MySteps = ({ onStepComplete, onComplete }) => {
return (
<Steps>
<Step1 onStepComplete={onStepComplete} />
<Step2 onComplete={onComplete} />
</Steps>
);
};
const Step1 = ({ onStepComplete }) => {
const { register, handleSubmit } = useForm();
const { next } = useSteps();
const onSubmit = (data) => {
onStepComplete(data);
next();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>Step 1</h1>
<input type="file" {...register("myFile")} />
<button>Next</button>
</form>
);
};
const Step2 = ({ onComplete }) => {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => {
onComplete(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>Step 2</h1>
<input type="text" {...register("name")} />
<button>Submit</button>
</form>
);
};
https://codesandbox.io/s/condescending-leaf-6gzoj?file=/src/App.js
Passing down the function and tracking changes at parent level is not a great idea. react-hook-form provides a form context option which allows you to do this independently. Such that errors, onChange are handled in each step separately. But when you need to submit the data you can get all of those in the parent component.
Refer to this documentation: https://react-hook-form.com/api/useformcontext/
Note: Many people make the mistake of placing the FormProvider inside the parent component. Remember that FormProvider should wrap the Parent component as well.
"I am trying to add comment functionality in my react application and I want to call different functions depending on my user type"
"I have successfully applied the logic and also I am able to post comments and save it."
Here's my Code for CommentForm.js
import React, { useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addComment } from '../../actions/userdata';
const CommentForm = ({ userdataId, addComment }) => {
const [text, setText] = useState('');
return (
<Fragment>
{/* <div className='bg-primary p'>
<h3>Leave a Comment</h3>
</div> */}
<form
className='form my-1'
onSubmit={e => {
e.preventDefault();
addComment(userdataId, { text });
setText('');
}}
>
<textarea
name='text'
cols='3'
rows='3'
placeholder='Any Other Comments..'
value={text}
onChange={e => setText(e.target.value)}
required
/>
<input type='submit' className='btn btn-dark my-1' value='Submit' />
<small> {'<==='} Press this button to save Comments</small>
</form>
</Fragment>
);
};
CommentForm.propTypes = {
addComment: PropTypes.func.isRequired
};
export default connect(
null,
{ addComment }
)(CommentForm);
and I am using this in some other file as
<CommentForm userdataId={userdata._id}/>
The problem is that I have around 9 functions like addComment say addcommenti, addcommentii, addcommentiii,.... and so on in ../actions/userdata.
and Hence I'll have to create 9 files.
I already tried
<CommentForm userdataId={userdata._id} addComment={addCommenti}/>
but it's not working i.e it's making call to addComment and not to addCommenti .
Any ideas on how to proceed ?
Here's my addComment Function
//Add Comment
export const addComment = (userdataId, formData) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post(
`/api/userdata/comment/${userdataId}`,
formData,
config
);
dispatch({
type: ADD_COMMENT,
payload: res.data
});
dispatch(setAlert('Comment Added', 'success'));
} catch (err) {
dispatch({
type: USERDATA_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
You have an imported function and a prop with the same name, that seems to cause the problem here: You think that you access the prop, but actually your component calls the imported function.
Try to remove the object destructuring in your CommentForm Component function and call this code instead:
// ...
const CommentForm = (props) => { /* <<-- HERE */
const [text, setText] = useState('');
return (
<Fragment>
<form
className='form my-1'
onSubmit={e => {
e.preventDefault();
props.addComment(props.userdataId, { text }); /* <<-- HERE */
setText('');
}}
>
<textarea
name='text'
cols='3'
rows='3'
placeholder='Any Other Comments..'
value={text}
onChange={e => setText(e.target.value)}
required
/>
<input type='submit' className='btn btn-dark my-1' value='Submit' />
<small> {'<==='} Press this button to save Comments</small>
</form>
</Fragment>
);
};
// ...
This will explicitly use the function that you pass into the props and can not call the imported function by accident.
Though, you could also rename the prop to something like onAddComment so your prop and imported function always use different names (which makes the easier to understand, in my opinion)
So two things:
If you need to use 9 functions in your component, even if you only need one per user type, I would pass all 9 functions down and handle the logic for which to use there.
In this case you just want to add the remaining functions as you have already.
If you do want to override an action which is being injected using redux so that you can decide which function to pass in the parent component you can do it using the functional version of mapDispatchToProp; it has a second argument ownProps which you can use:
const mapDispatchToProps = (dispatch, ownProps) => ({
addComment: (text, userdataid) => dispatch(addComment(userdataid, { text }),
...ownProps,
});
because I have spread ownProps after declaring addComment, you can pass addComment from the parent and it will replace the one we declared above.
I had a search bar component in react and I wanted to have either an onSubmit prop to it or an onChange prop, but not both of them together. How do I implement it in the best way possible?
I tried using if-else statements but the code doesn't look elegant to me.
class QueryBar extends PureComponent {
render() {
const { placeholder, leftIcon, onSubmit, onChange, width } = this.props;
return (
<form
style={{ width }}
onSubmit={e => {
e.preventDefault();
onSubmit(e.target[0].value);
}}
>
<InputGroup
placeholder={placeholder}
width={width}
leftIcon="search"
rightElement={
<Button
type="submit"
icon={leftIcon}
minimal={true}
intent={Intent.PRIMARY}
/>
}
/>
</form>
);
}
}
QueryBar.propTypes = {
width: PropTypes.number,
placeholder: PropTypes.string,
leftIcon: PropTypes.oneOfType(['string', 'element']),
onSubmit: PropTypes.func
};
QueryBar.defaultProps = {
placeholder: 'Search...',
leftIcon: 'arrow-right',
width: 360
};
export default QueryBar;
You can use the ternary operator (?) instead of if-else.
const { placeholder, leftIcon, onSubmit, onChange, width } = this.props;
const handleSubmit = onSubmit ? onSubmit : onChange;
And use the same function later
<form
style={{ width }}
onSubmit={e => {
e.preventDefault();
handleSubmit(e.target[0].value);
}}
>
I am looking to create a stateless component who's input element can be validated by the parent component.
In my example below, I am running into a problem where the input ref is never being assigned to the parent's private _emailAddress property.
When handleSubmit is called, this._emailAddress is undefined. Is there something I'm missing, or is there a better way to do this?
interface FormTestState {
errors: string;
}
class FormTest extends React.Component<void, FormTestState> {
componentWillMount() {
this.setState({ errors: '' });
}
render(): JSX.Element {
return (
<main role='main' className='about_us'>
<form onSubmit={this._handleSubmit.bind(this)}>
<TextInput
label='email'
inputName='txtInput'
ariaLabel='email'
validation={this.state.errors}
ref={r => this._emailAddress = r}
/>
<button type='submit'>submit</button>
</form>
</main>
);
}
private _emailAddress: HTMLInputElement;
private _handleSubmit(event: Event): void {
event.preventDefault();
// this._emailAddress is undefined
if (!Validators.isEmail(this._emailAddress.value)) {
this.setState({ errors: 'Please enter an email address.' });
} else {
this.setState({ errors: 'All Good.' });
}
}
}
const TextInput = ({ label, inputName, ariaLabel, validation, ref }: { label: string; inputName: string; ariaLabel: string; validation?: string; ref: (ref: HTMLInputElement) => void }) => (
<div>
<label htmlFor='txt_register_first_name'>
{ label }
</label>
<input type='text' id={inputName} name={inputName} className='input ' aria-label={ariaLabel} ref={ref} />
<div className='input_validation'>
<span>{validation}</span>
</div>
</div>
);
You can useuseRef hook which is available since v16.7.0-alpha.
EDIT: You're encouraged to use Hooks in production as of 16.8.0 release!
Hooks enable you to maintain state and handle side effects in functional components.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Read more in Hooks API documentation
EDIT: You now can with React Hooks. See the answer by Ante Gulin.
You can't access React like methods (like componentDidMount, componentWillReceiveProps, etc) on stateless components, including refs. Checkout this discussion on GH for the full convo.
The idea of stateless is that there isn't an instance created for it (state). As such, you can't attach a ref, since there's no state to attach the ref to.
Your best bet would be to pass in a callback for when the component changes and then assign that text to the parent's state.
Or, you can forego the stateless component altogether and use an normal class component.
From the docs...
You may not use the ref attribute on functional components because they don't have instances. You can, however, use the ref attribute inside the render function of a functional component.
function CustomTextInput(props) {
// textInput must be declared here so the ref callback can refer to it
let textInput = null;
function handleClick() {
textInput.focus();
}
return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
This is late but I found this solution much better.
Pay attention to how it uses useRef & how properties are available under current property.
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
For more reference check react docs
The value of your TextInput is nothing more than a state of your component. So instead of fetching the current value with a reference (bad idea in general, as far as I know) you could fetch the current state.
In a reduced version (without typing):
class Form extends React.Component {
constructor() {
this.state = { _emailAddress: '' };
this.updateEmailAddress = this.updateEmailAddress.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
updateEmailAddress(e) {
this.setState({ _emailAddress: e.target.value });
}
handleSubmit() {
console.log(this.state._emailAddress);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
value={this.state._emailAddress}
onChange={this.updateEmailAddress}
/>
</form>
);
}
}
You can also get refs into functional components with a little plumbing
import React, { useEffect, useRef } from 'react';
// Main functional, complex component
const Canvas = (props) => {
const canvasRef = useRef(null);
// Canvas State
const [canvasState, setCanvasState] = useState({
stage: null,
layer: null,
context: null,
canvas: null,
image: null
});
useEffect(() => {
canvasRef.current = canvasState;
props.getRef(canvasRef);
}, [canvasState]);
// Initialize canvas
useEffect(() => {
setupCanvas();
}, []);
// ... I'm using this for a Konva canvas with external controls ...
return (<div>...</div>);
}
// Toolbar which can do things to the canvas
const Toolbar = (props) => {
console.log("Toolbar", props.canvasRef)
// ...
}
// Parent which collects the ref from Canvas and passes to Toolbar
const CanvasView = (props) => {
const canvasRef = useRef(null);
return (
<Toolbar canvasRef={canvasRef} />
<Canvas getRef={ ref => canvasRef.current = ref.current } />
}