feat(client): use React Final Form instead of Redux Form (#36742)

This commit is contained in:
Valeriy
2019-09-04 15:48:58 +03:00
committed by mrugesh
parent 271d25a2ab
commit d85425fd1b
9 changed files with 310 additions and 253 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { reduxForm } from 'redux-form';
import { Form } from 'react-final-form';
import {
FormFields,
@@ -12,16 +12,7 @@ import {
const propTypes = {
buttonText: PropTypes.string,
enableSubmit: PropTypes.bool,
errors: PropTypes.object,
fields: PropTypes.objectOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired
})
),
formFields: PropTypes.arrayOf(PropTypes.string).isRequired,
handleSubmit: PropTypes.func,
hideButton: PropTypes.bool,
id: PropTypes.string.isRequired,
initialValues: PropTypes.object,
@@ -33,67 +24,44 @@ const propTypes = {
submit: PropTypes.func.isRequired
};
export function DynamicForm({
// redux-form
errors,
fields,
handleSubmit,
fields: {
// eslint-disable-next-line react/prop-types
_meta: { allPristine }
},
// HOC
function DynamicForm({
id,
formFields,
initialValues,
options,
submit,
buttonText,
enableSubmit,
hideButton,
id,
options,
submit
hideButton
}) {
return (
<form
id={`dynamic-${id}`}
onSubmit={handleSubmit((values, ...args) =>
<Form
initialValues={initialValues}
onSubmit={(values, ...args) =>
submit(formatUrlValues(values, options), ...args)
)}
style={{ width: '100%' }}
}
>
<FormFields
errors={errors}
fields={fields}
formId={id}
options={options}
/>
<BlockSaveWrapper>
{hideButton ? null : (
<BlockSaveButton
disabled={
(allPristine && !enableSubmit) ||
!!Object.keys(errors).filter(key => errors[key]).length
}
>
{buttonText ? buttonText : null}
</BlockSaveButton>
)}
</BlockSaveWrapper>
</form>
{({ handleSubmit, pristine, error }) => (
<form
id={`dynamic-${id}`}
onSubmit={handleSubmit}
style={{ width: '100%' }}
>
<FormFields fields={formFields} options={options} />
<BlockSaveWrapper>
{hideButton ? null : (
<BlockSaveButton disabled={(pristine && !enableSubmit) || error}>
{buttonText ? buttonText : null}
</BlockSaveButton>
)}
</BlockSaveWrapper>
</form>
)}
</Form>
);
}
DynamicForm.displayName = 'DynamicForm';
DynamicForm.propTypes = propTypes;
const DynamicFormWithRedux = reduxForm()(DynamicForm);
export default function Form(props) {
return (
<DynamicFormWithRedux
{...props}
fields={props.formFields}
form={props.id}
/>
);
}
Form.propTypes = propTypes;
export default DynamicForm;

View File

@@ -1,30 +1,14 @@
/* global expect */
/* global jest, expect */
import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import renderer from 'react-test-renderer';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { render, fireEvent } from '@testing-library/react';
import { DynamicForm } from './Form';
Enzyme.configure({ adapter: new Adapter() });
import Form from './Form';
const defaultTestProps = {
errors: {},
fields: {
_meta: {
allPristine: true,
name: 'name',
onChange: () => {},
value: ''
}
},
handleSubmit: () => {},
buttonText: 'Submit',
enableSubmit: true,
formFields: ['name', 'website'],
hideButton: false,
id: 'my-test-form',
options: {
types: {
@@ -36,8 +20,69 @@ const defaultTestProps = {
submit: () => {}
};
test('<DynamicForm /> snapshot', () => {
const component = renderer.create(<DynamicForm {...defaultTestProps} />);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
test('should render', () => {
const { getByLabelText, getByText } = render(<Form {...defaultTestProps} />);
const nameInput = getByLabelText(/name/i);
expect(nameInput).not.toBeRequired();
expect(nameInput).toHaveAttribute('type', 'text');
const websiteInput = getByLabelText(/website/i);
expect(websiteInput).toBeRequired();
expect(websiteInput).toHaveAttribute('type', 'url');
const button = getByText(/submit/i);
expect(button).toHaveAttribute('type', 'submit');
expect(button).toBeDisabled();
});
test('should render with default values', () => {
const websiteValue = 'http://mysite.com';
const nameValue = 'John';
const { getByLabelText, getByText } = render(
<Form
{...defaultTestProps}
enableSubmit={true}
initialValues={{ name: nameValue, website: websiteValue }}
/>
);
const nameInput = getByLabelText(/name/i);
expect(nameInput).toHaveValue(nameValue);
const websiteInput = getByLabelText(/website/i);
expect(websiteInput).toHaveValue(websiteValue);
const button = getByText(/submit/i);
expect(button).toBeEnabled();
});
test('should submit', () => {
const submit = jest.fn();
const props = {
...defaultTestProps,
submit
};
const websiteValue = 'http://mysite.com';
const { getByLabelText, getByText } = render(<Form {...props} />);
const websiteInput = getByLabelText(/website/i);
fireEvent.change(websiteInput, { target: { value: websiteValue } });
expect(websiteInput).toHaveValue(websiteValue);
const button = getByText(/submit/i);
expect(button).toBeEnabled();
fireEvent.click(button);
expect(submit).toHaveBeenCalledTimes(1);
expect(submit.mock.calls[0][0]).toEqual({ website: websiteValue });
fireEvent.change(websiteInput, { target: { value: `${websiteValue}///` } });
expect(websiteInput).toHaveValue(`${websiteValue}///`);
fireEvent.click(button);
expect(submit).toHaveBeenCalledTimes(2);
expect(submit.mock.calls[1][0]).toEqual({ website: websiteValue });
});

View File

@@ -9,20 +9,11 @@ import {
FormGroup,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { Field } from 'react-final-form';
const propTypes = {
errors: PropTypes.objectOf(PropTypes.string),
fields: PropTypes.objectOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired
})
).isRequired,
fields: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
options: PropTypes.shape({
errors: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(null)])
),
ignored: PropTypes.arrayOf(PropTypes.string),
placeholder: PropTypes.bool,
required: PropTypes.arrayOf(PropTypes.string),
@@ -31,7 +22,7 @@ const propTypes = {
};
function FormFields(props) {
const { errors = {}, fields, options = {} } = props;
const { fields, options = {} } = props;
const {
ignored = [],
placeholder = true,
@@ -40,38 +31,43 @@ function FormFields(props) {
} = options;
return (
<div>
{Object.keys(fields)
{fields
.filter(field => !ignored.includes(field))
.map(key => fields[key])
.map(({ name, onChange, value, pristine }) => {
const key = kebabCase(name);
const type = name in types ? types[name] : 'text';
return (
<Col key={key} xs={12}>
<FormGroup>
{type === 'hidden' ? null : (
<ControlLabel htmlFor={key}>{startCase(name)}</ControlLabel>
)}
<FormControl
componentClass={type === 'textarea' ? type : 'input'}
id={key}
name={name}
onChange={onChange}
placeholder={placeholder ? name : ''}
required={required.includes(name)}
rows={4}
type={type}
value={value}
/>
{name in errors && !pristine ? (
<HelpBlock>
<Alert bsStyle='danger'>{errors[name]}</Alert>
</HelpBlock>
) : null}
</FormGroup>
</Col>
);
})}
.map(name => (
<Field key={`${name}-field`} name={name}>
{({ input: { value, onChange }, meta: { pristine, error } }) => {
const key = kebabCase(name);
const type = name in types ? types[name] : 'text';
return (
<Col key={key} xs={12}>
<FormGroup>
{type === 'hidden' ? null : (
<ControlLabel htmlFor={key}>
{startCase(name)}
</ControlLabel>
)}
<FormControl
componentClass={type === 'textarea' ? type : 'input'}
id={key}
name={name}
onChange={onChange}
placeholder={placeholder ? name : ''}
required={required.includes(name)}
rows={4}
type={type}
value={value}
/>
{error && !pristine ? (
<HelpBlock>
<Alert bsStyle='danger'>{error}</Alert>
</HelpBlock>
) : null}
</FormGroup>
</Col>
);
}}
</Field>
))}
</div>
);
}

View File

@@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DynamicForm /> snapshot 1`] = `
<form
id="dynamic-my-test-form"
style={
Object {
"width": "100%",
}
}
>
<div>
<div
className="col-xs-12"
>
<div
className="form-group"
>
<label
className="control-label"
htmlFor="name"
>
Name
</label>
<input
className="form-control"
id="name"
name="name"
onChange={[Function]}
placeholder="name"
required={false}
rows={4}
type="text"
value=""
/>
</div>
</div>
</div>
<div
style={
Object {
"padding": "0 15px",
}
}
>
<button
className="btn btn-primary btn-block"
disabled={false}
type="submit"
>
Submit
</button>
</div>
</form>
`;

View File

@@ -18,7 +18,7 @@ export function callIfDefined(fn) {
export function formatUrlValues(values, options) {
return Object.keys(values).reduce((result, key) => {
let value = values[key];
if (options.types[key] === 'url') {
if (value && options.types[key] === 'url') {
value = normalizeUrl(value, normalizeOptions);
}
return { ...result, [key]: value };