My application has a lot of redux-form. I am using Jest and Enzyme for unit testing. However, I fail to test the redux-form. My component is a login form like:
import { login } from './actions';
export class LoginForm extends React.Component<any, any> {
onSubmit(values) {
this.props.login(values, this.props.redirectUrl);
}
render() {
const { handleSubmit, status, invalid } = this.props;
return (
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<TextField label="Email" name="email">
<TextField type="password" label="Password" name="password" autoComplete/>
<Button submit disabled={invalid} loading={status.loading}>
OK
</Button>
</form>
);
}
}
const mapStateToProps = (state) => ({
status: state.login.status,
});
const mapDispatchToProps = { login };
const form = reduxForm({ form: 'login' })(LoginForm);
export default connect(mapStateToProps, mapDispatchToProps)(form);
Mock the store, Import connected component
redux-form uses the store to maintain the form inputs. I then use redux-mock-store:
import ConnectedLoginForm from './LoginForm';
const configureStore = require('redux-mock-store');
const store = mockStore({});
const spy = jest.fn();
const wrapper = shallow(
<Provider store={store}>
<ConnectedLoginForm login={spy}/>
</Provider>);
wrapper.simulate('submit');
expect(spy).toBeCalledWith();
But in this way, the submit is not simulated, my test case failed:
Expected mock function to have been called with: []
But it was not called.
Mock the store, Import React component only.
I tried to create redux form from the testing code:
import { Provider } from 'react-redux';
import ConnectedLoginForm, { LoginForm } from './LoginForm';
const props = {
status: new Status(),
login: spy,
};
const ConnectedForm = reduxForm({
form: 'login',
initialValues: {
email: 'test#test.com',
password: '000000',
},
})(LoginForm);
const wrapper = shallow(
<Provider store={store}>
<ConnectedForm {...props}/>
</Provider>);
console.log(wrapper.html());
wrapper.simulate('submit');
expect(spy).toBeCalledWith({
email: 'test#test.com',
password: '000000',
});
In this case, i still got error of function not called. If I add console.log(wrapper.html()), I got error:
Invariant Violation: Could not find "store" in either the context or
props of "Connect(ConnectedField)". Either wrap the root component in
a , or explicitly pass "store" as a prop to
"Connect(ConnectedField)".
I cannot find documentations on official sites of redux-form or redux or jest/enzyme, or even Google.. Please help, thanks.
I used the real store (as redux-mock-store does not support reducers) and redux-form's reducer, it worked for me. Code example:
import { createStore, Store, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { reducer as formReducer } from 'redux-form';
const rootReducer = combineReducers({
form: formReducer,
});
let store;
describe('Redux Form', () => {
beforeEach(() => {
store = createStore(rootReducer);
});
it('should submit form with form data', () => {
const initialValues = {...};
const onSubmit = jest.fn();
const wrapper = mount(
<Provider store={store}>
<SomeForm
onSubmit={onSubmit}
initialValues={initialValues}
/>
</Provider>
);
const form = wrapper.find(`form`);
form.simulate('submit');
const expectedFormValue = {...};
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0]).toEqual(expectedFormValue);
});
});
You can find the answer here: https://github.com/tylercollier/redux-form-test
In short, you can use shallow dive() function to test higher-order component, but in your case, you have a higher-order component inside a higher-order component.
You need to break you component into two components, the first one is a presentation component, without
const form = reduxForm({ form: 'login' })(LoginForm);
export default connect(mapStateToProps, mapDispatchToProps)(form);
You then wrap the first component into the second component (container component).
You can easily test the first component (presentation component)
I had the similar problem. The answer can be found here https://github.com/airbnb/enzyme/issues/1002.
Long story short, you should pass store as a prop into your form and use .dive() function on the wrapper.
Regards
Pavel
I made a tool which helps with problems like that. It make a test-cases with real data (chrome extension collect it and save to file) which you can run by CLI tool.
I recommend you to try it: https://github.com/wasteCleaner/check-state-management
Related
I'm working on a web app with NextJS, Apollo and React (hooks).
I have a form that asks the name of the visitor as the first step in a registration process.
When submitting the form the name will be saved in the Apollo cache and the visitor gets redirected to the next page.
import React, { useState } from 'react';
import Router , {useRouter} from 'next/router';
import { useApolloClient } from '#apollo/react-hooks';
const NameForm = props => {
const [name, setName] = useState("");
const client = useApolloClient();
const router = useRouter();
const handleSubmit = e => {
e.preventDefault();
if(!name) return;
client.writeData({ data: { name } });
router.push('/user/register');
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Naam</label>
<div>
<input type="text" id="name" name="name" value={name} onChange={e => setName(e.target.value)} />
<button type="submit" onClick={handleSubmit}>Get started</button>
</div>
</div>
</form>
)
}
export default NameForm;
The next page contains a more extensive form. When visitors come from the homepage, the name is already known and I want to get it back from the cache. I thought
import { gql } from 'apollo-boost';
import { useApolloClient } from '#apollo/react-hooks';
import AddUserForm from '../../components/forms/AddUserForm';
const GET_NAME = gql`
query GetName {
name #client
}`;
const AddUser = ({ name }) => (
<React.Fragment>
<AddUserForm name={name} />
</React.Fragment>
)
AddUser.getInitialProps = async ctx => {
const client = useApolloClient();
const name = await client.cache.readQuery({ query: GET_NAME });
return { name: name || '' };
}
export default AddUser;
I thought I could do this in the getInititialProps hooks are only allowed in the body of a functional component.
Because of the continuous development of next, react hooks and apollo I'm missing a tutorial/course about this and I find it difficult to find a right way to do this.
I hope someone here can help me further.
use apollo-client cache can lead you to some questions that really depends on the apollo-client's implementation and nextjs implementation.
If you open your app by entering the url to the browser address bar, Next.js will make requests (assuming the view need to fetch data) from server-side, then send to the client the rendered HTML.
Because apollo-client fetch then cache the data from server side, then the question is "Does Next.js send the apollo-client with its cache to client side for next request?"
You cannot sure about this unless you understand clearly about Next.js and apollo-client cache (about its implementation or how it works inside, if apollo cache data in-memory on server-side, you will fail if you go this way)
The answer is unsure because it depends on two stuffs at the same time. And maybe changed on the future!
So to deal with this problem, just use the Next.js way, it has designed a tunnel for data, it is the query on the url.
const NameForm = props => {
const [name, setName] = useState("");
const client = useApolloClient();
const router = useRouter();
const handleSubmit = e => {
e.preventDefault();
if(!name) return;
router.push(`/user/register?name=${name}`);
}
//render ...
}
import { useRouter } from 'next/router';
import AddUserForm from '../../components/forms/AddUserForm';
const AddUser = () => {
const router = useRouter();
return (
<React.Fragment>
<AddUserForm name={router.query.name} />
</React.Fragment>
)
}
export default AddUser;
If you want to send an object instead of a string?
const data = { name: "FoxeyeRinx", email: "foxeye.rinx#gmail.com" };
const base64 = btoa(JSON.stringify(data));
router.push(`/user/register?data=${base64}`);
const AddUser = () => {
const router = useRouter();
const base64 = router.query.data;
//decode base64 then parse it to js object
const data = JSON.parse(atob(base64));
return (
<React.Fragment>
<AddUserForm data={data}/>
</React.Fragment>
)
}
If you think the query is ugly and want to hide the query, use this guide: https://nextjs.org/learn/basics/clean-urls-with-dynamic-routing
I am building a React form in the CRM pane of Flex which will POST data to an external service when the agent fills out the form and hits a submit button. Using the example code from create-flex-plugin from Plugin Builder v3, I have successfully persisted the data from a form field in redux. However, as an agent in Flex, if I have multiple reservations open, when I toggle between them they use the same data. How do I design my form so that an agent can enter data in the form for different reservations and they are kept separately?
I am relatively new to Flex, React and Redux, and not sure if there is anything Flex-specific about what I need to do, especially when handling multiple concurrent reservations. I've thought of keeping a Map keyed by reservationId or taskId in redux, but it's not clear how I'd pass the taskId into the reducer. I am also not sure if other tools like redux-form will play nice with Flex's design.
Being pointed in the right direction or getting some sample code would be a great help.
My current implementation, which persists a field called 'subcategory', looks like this:
HrmFormState.js
const UPDATE_FORM = 'UPDATE_FORM';
const initialState = {
subcategory: 'my category',
};
export class Actions {
static updateForm = (e) => ({ type: UPDATE_FORM, text: e.target.value });
}
export function reduce(state = initialState, action) {
switch (action.type) {
case UPDATE_FORM: {
return {
...state,
subcategory: action.text,
};
}
default:
return state;
}
}
HrmForm.Container.js
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Actions } from '../../states/HrmFormState';
import HrmForm from './HrmForm';
const mapStateToProps = (state) => ({
subcategory: state['hrm-form'].hrmForm.subcategory,
});
const mapDispatchToProps = (dispatch) => ({
updateForm: bindActionCreators(Actions.updateForm, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(HrmForm);
HrmForm.jsx (snippet)
render() {
if (!this.props.task) {
return null;
}
return (
<HrmFormComponentStyles>
<form onSubmit={this.handleSubmit}>
<label>
Subcategory:
<input type="text" value={this.props.subcategory}
onChange={this.props.updateForm} />
</label>
<input type="submit" value="Submit" />
</form>
</HrmFormComponentStyles>
);
}
I'm trying to integrate a React-Toolbox Input component with Redux-Form. However, the Input component remains empty when typing. I'm using https://github.com/react-toolbox/react-toolbox/issues/1293 as a guide for the integration.
import React from 'react'
import PropTypes from 'prop-types'
import { Field, reduxForm } from 'redux-form'
import Input from 'react-toolbox/lib/input'
const renderField = ({ input, meta, ...props }) => (
<Input
{ ...input }
{ ...props }
error={ meta.touched && meta.error } />
)
const Form = ({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field
name="myTextField"
component={renderField}
type="text"
/>
</form>
)
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
}
export default reduxForm({
form: 'myForm',
})(Form)
This is using react-toolbox 2.0.0-beta.12 and redux-form 7.2.0
You use input, meta and another ...props in your "functional component" renderField, but renderField props argument is named field and is not used anywhere.
You should change renderField this way:
const renderField = ({ input, meta, ...props }) => (
<Input
{ ...input }
{ ...props }
error={ meta.touched && meta.error }
/>
);
UPD
redux-form Basic Usage Guide says:
The redux store should know how to handle actions coming from the form components. To enable this, we need to pass the formReducer to your store. It serves for all of your form components, so you only have to pass it once.
So you should pass formReducer to your store:
import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'
const rootReducer = combineReducers({
// ...your other reducers here
// you have to pass formReducer under 'form' key,
// for custom keys look up the docs for 'getFormState'
form: formReducer
})
const store = createStore(rootReducer)
I have written code, which uses a Modal dialog to display a form.
My react app is rendered at "root"
index.html
<div id="root"></div>
App.js
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<ExampleBasic/>
</Provider>
, document.getElementById('root'));
ExmpleBasic.js
Please ignore state management in component here. this is just for example.
import React, { PureComponent } from 'react';
import Lorem from 'react-lorem-component';
import Modal from '#atlaskit/modal-dialog';
import Button from '#atlaskit/button';
export default class ExampleBasic extends PureComponent {
state = { isOpen: false }
open = () => this.setState({ isOpen: true })
close = () => this.setState({ isOpen: false })
secondaryAction = ({ target }) => console.log(target.innerText)
render() {
const { isOpen } = this.state;
const actions = [
{ text: 'Close', onClick: this.close },
{ text: 'Secondary Action', onClick: this.secondaryAction },
];
return (
<div>
<Button onClick={this.open}>Open Modal</Button>
{isOpen && (
<Modal
actions={actions}
onClose={this.close}
heading="Modal Title"
>
<BasicFormContainer />
</Modal>
)}
</div>
);
}
}
BasicFormContainer.js
const mapStateToProps = state => ({
addDesignation: state.designations.addDesignation,
});
const mapDispatchToProps = dispatch => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicForm);
BasicForm.js
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
class BasicForm extends Component {
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
}
submit(values) {
console.log(values);
}
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit(this.submit)}>
<Field
name="designationName"
component="input"
placeholder="Name"
label="Enter name"
autoFocus
/>
</form>
);
}
}
export default reduxForm({
form: 'BasicForm',
enableReinitialize: true,
})(BasicForm);
However modal is rendered using portal, outside current DOM.
As modal is rendered outside the scope of redux context, it is not getting the
store. and i am getting an error "Uncaught Error: Field must be inside a component decorated with reduxForm()"
Below is link to same kind of problem, where redux form within portal is not working.
Redux Form Wrapped Inside Custom Portal Component?
in React 16 it is handled by portals, but version before then that you can try something like as follow.
export default class ExampleBasic extends PureComponent {
...
static contextTypes = { store: React.PropTypes.object };
render() {
const { isOpen } = this.state;
const actions = [
{ text: 'Close', onClick: this.close },
{ text: 'Secondary Action', onClick: this.secondaryAction },
];
return (
<div>
<Button onClick={this.open}>Open Modal</Button>
{isOpen && (
<Modal
actions={actions}
onClose={this.close}
heading="Modal Title"
>
<Provider store={this.context.store}>
<BasicFormContainer />
</Provider>
</Modal>
)}
</div>
);
}
}
You need to pass in the values of BasicForm.js to the Redux store and dispatch an action from there itself and not from the BasicFormContainer.js. This way, the Modal remains inside of the scope of your root element and thus there is no need to access the store outside of the Provider.
Then update the Redux store based on the values entered in the form. Once, the store is updated, you can then access it from anywhere in your application such as Modal in your case.
I downgraded to version 2.1.0 to solve the problem.
Have to create an edit Page editing a number of parameters on an instance of a'tale' resource.
However adding any element such as an MUI Card or even a div, is causing the app to freeze in various ways.
These are the approaches I have tried.
1) Adding a card component or placing my elements within a div for styling
export const EditorEditTale = (props) => {
return (
<Edit {...props} title="Tale Editor">
<SimpleForm >
<div>
<Image />
<TaleCardHeader props={ props } style={taleCardHeaderStyle.editor} />
</div>
</SimpleForm>
</Edit>
)
};
This is causing nothing to render.
Second approach, assuming that the record and basePath arent getting propagated to the children completely. Trying to use component like below.
const Input = ({record, basePath}) => {
return (
<div>
<LongTextInput source="taleText" />
</div>
)
}
This is causing the page to not render with everything in some kind of locking loop with the error - cannot read property touched of undefined.
How should I create a custom Edit page with a complex inputs and styling.
UPDATE: Been trying to write a custom form to substitute the SimpleForm component with no luck so far.
To create a custom form you can follow these steps:
make an exact copy of SimpleForm to your project.
rename SimpleForm to what you want.
fix all the relative imports.
test the new form until it works.
I made a minimum working form based on current master branch's SimpleForm
import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import { reduxForm, Field } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import getDefaultValues from 'admin-on-rest/mui/form/getDefaultValues';
import FormField from 'admin-on-rest/mui/form/FormField';
import Toolbar from 'admin-on-rest/mui/form/Toolbar';
const formStyle = { padding: '0 1em 1em 1em' };
export class PostForm extends Component {
handleSubmitWithRedirect = (redirect = this.props.redirect) => this.props.handleSubmit(values => this.props.save(values, redirect));
render() {
const { children, invalid, record, resource, basePath, submitOnEnter, toolbar } = this.props;
return (
<form className="simple-form">
<Field name="name_of_a_field" component="input" />
{toolbar && React.cloneElement(toolbar, {
handleSubmitWithRedirect: this.handleSubmitWithRedirect,
invalid,
submitOnEnter,
})}
</form>
);
}
}
PostForm.propTypes = {
basePath: PropTypes.string,
children: PropTypes.node,
defaultValue: PropTypes.oneOfType([
PropTypes.object,
PropTypes.func,
]),
handleSubmit: PropTypes.func, // passed by redux-form
invalid: PropTypes.bool,
record: PropTypes.object,
resource: PropTypes.string,
redirect: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
save: PropTypes.func, // the handler defined in the parent, which triggers the REST submission
submitOnEnter: PropTypes.bool,
toolbar: PropTypes.element,
validate: PropTypes.func,
};
PostForm.defaultProps = {
submitOnEnter: true,
toolbar: <Toolbar />,
};
const enhance = compose(
connect((state, props) => ({
initialValues: getDefaultValues(state, props),
})),
reduxForm({
form: 'record-form',
enableReinitialize: true,
}),
);
export default enhance(PostForm);
The above code works for AOR's example.
I hope this helps.
(import might be slightly different when you have AOR as npm dependency :
import getDefaultValues from 'admin-on-rest/lib/mui/form/getDefaultValues';
import FormField from 'admin-on-rest/lib/mui/form/FormField';
import Toolbar from 'admin-on-rest/lib/mui/form/Toolbar';
)
Documenting my final answer. You have to create a custom Redux Form. You can use AOR Input components straight. They come prewrapped for Redux Form.
import { Field, reduxForm } from 'redux-form';
import compose from 'recompose/compose';
import { connect } from 'react-redux';
class StyledForm extends Component {
// Newer version of aor needs this function defined and passed to save buttons. All props are being passed by parent List component.
handleSubmitWithRedirect = (redirect = this.props.redirect) => this.props.handleSubmit(values => this.props.save(values, redirect));
render() {
const { handleSubmit, invalid, record, resource, basePath } = this.props
return (<div>
<form onSubmit={handleSubmit} >
<Card >
<CardText >
//This component simply displays data, something not possible very easily with SimpleForm.
<HeaderComp basePath={basePath} record={record} />
<Field source="category_id"
optionText="categoryName"
reference="categories"
resource={resource}
record={record}
basePath={basePath}
name="NAME OF THE FIELD IN YOUR REDUX DATASTORE"
component={REFERENCEFIELDCOMP} />
//create complex div structures now.
<div >
<span>Tale</span>
<Field resource={resource} record={record} basePath={basePath} name="taleText" component={TextInput} />
</div>
</CardText >
<MuiToolbar>
<ToolbarGroup>
<SaveButton handleSubmitWithRedirect={this.handleSubmitWithRedirect}/>
//Add custom buttons with custom actions
<Field record={record} name="status" component={EditButtons} />
</ToolbarGroup>
</MuiToolbar>
</Card>
</form>
</div>)
}
};
const enhance = compose(
connect((state, props) => ({
initialValues: getDefaultValues(state, props),
})),
reduxForm({
form: 'record-form',
enableReinitialize: true,
}),
);
export default enhance(StyledForm);
You will have to either import or copy getDefaultValues from AOR in the node modules.
I copied it into the file below.
import getDefaultValues from '../functions/getDefaultValues';
If you need a referenceField in your field. Then wrap it in a custom component like shown below
const DropDownSelector = ({optionText, ...props}) => {
return (
<ReferenceInput {...props} label="" >
<SelectInput optionText={optionText} />
</ReferenceInput>
)
}