diff --git a/package.json b/package.json index cb161307..38f071d5 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,15 @@ "classnames": "^2.2.5", "compression": "^1.7.2", "cookie-parser": "^1.4.3", + "core-js": "^2.5.0", "decimal.js": "7.2.4", "dotenv": "4.0.0", "dotenv-expand": "4.0.1", "express": "^4.16.3", "express-enforces-ssl": "^1.1.0", "express-sitemap": "^1.8.0", + "final-form": "^4.4.0", + "final-form-arrays": "^1.0.4", "helmet": "^3.12.0", "lodash": "^4.17.5", "moment": "^2.20.1", @@ -26,21 +29,23 @@ "path-to-regexp": "^2.2.0", "prop-types": "^15.6.1", "query-string": "^5.1.1", + "raf": "3.4.0", "raven": "^2.4.2", "raven-js": "^3.24.0", - "react": "^15.6.2", - "react-addons-shallow-compare": "^15.6.2", + "react": "^16.3.1", "react-dates": "^16.0.0", - "react-dom": "^15.6.2", + "react-dom": "^16.3.1", + "react-final-form": "^3.1.5", + "react-final-form-arrays": "^1.0.4", "react-google-maps": "^9.4.5", "react-helmet": "^5.2.0", "react-intl": "^2.4.0", "react-moment-proptypes": "^1.5.0", - "react-redux": "^5.0.6", + "react-redux": "^5.0.7", "react-router-dom": "^4.2.2", "react-sortable-hoc": "^0.6.8", "redux": "^3.7.2", - "redux-form": "^6.8.0", + "redux-form": "^7.3.0", "redux-thunk": "^2.2.0", "sanitize.css": "^5.0.0", "sharetribe-scripts": "1.1.2", @@ -51,14 +56,12 @@ }, "devDependencies": { "enzyme": "^3.3.0", - "enzyme-adapter-react-15": "^1.0.5", + "enzyme-adapter-react-16": "^1.1.1", "enzyme-to-json": "^3.3.3", "nodemon": "^1.17.2", "nsp": "^3.2.1", "nsp-preprocessor-yarn": "^1.0.1", - "prettier": "^1.11.1", - "react-addons-test-utils": "^15.6.2", - "react-test-renderer": "^15.6.2" + "prettier": "^1.11.1" }, "scripts": { "clean": "rm -rf build/*", diff --git a/src/components/EditListingFeaturesPanel/EditListingFeaturesPanel.js b/src/components/EditListingFeaturesPanel/EditListingFeaturesPanel.js index ed743977..d912fb1e 100644 --- a/src/components/EditListingFeaturesPanel/EditListingFeaturesPanel.js +++ b/src/components/EditListingFeaturesPanel/EditListingFeaturesPanel.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage } from 'react-intl'; -import { toPairs } from 'lodash'; import { ensureListing } from '../../util/data'; import { EditListingFeaturesForm } from '../../containers'; @@ -38,15 +37,8 @@ const EditListingFeaturesPanel = props => { ); - const currentFeaturesArray = publicData && publicData.amenities; - const currentFeatures = - currentFeaturesArray && - currentFeaturesArray.reduce((map, key) => { - map[key] = true; - return map; - }, {}); - - const initialValues = { [FEATURES_NAME]: currentFeatures }; + const amenities = publicData && publicData.amenities; + const initialValues = { amenities }; return (
@@ -56,9 +48,7 @@ const EditListingFeaturesPanel = props => { name={FEATURES_NAME} initialValues={initialValues} onSubmit={values => { - const entries = values[FEATURES_NAME] ? toPairs(values[FEATURES_NAME]) : []; - - const amenities = entries.filter(entry => entry[1] === true).map(entry => entry[0]); + const { amenities = [] } = values; const updatedValues = { publicData: { amenities }, diff --git a/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js b/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js index e3808494..4444b7db 100644 --- a/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js +++ b/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js @@ -40,10 +40,12 @@ const EditListingPoliciesPanel = props => { { + const { rules = '' } = values; const updateValues = { publicData: { - ...values, + rules, }, }; onSubmit(updateValues); diff --git a/src/components/FieldBirthdayInput/FieldBirthdayInput.example.js b/src/components/FieldBirthdayInput/FieldBirthdayInput.example.js index 1fdc217f..f413fc8e 100644 --- a/src/components/FieldBirthdayInput/FieldBirthdayInput.example.js +++ b/src/components/FieldBirthdayInput/FieldBirthdayInput.example.js @@ -1,37 +1,51 @@ /* eslint-disable no-console */ import React from 'react'; -import { reduxForm } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import * as validators from '../../util/validators'; import FieldBirthdayInput from './FieldBirthdayInput'; -const formName = 'Styleguide.BirthdayInput.Form'; +const FormComponent = props => ( + { + const { handleSubmit, onChange, values } = fieldRenderProps; + const required = validators.required('A valid date is required'); + const minAge = 18; + const minAgeRequired = validators.ageAtLeast(`Age should be at least ${minAge}`, minAge); -const FormComponent = () => { - const required = validators.required('A valid date is required'); - const minAge = 18; - const minAgeRequired = validators.ageAtLeast(`Age should be at least ${minAge}`, minAge); - return ( -
- - - ); -}; - -const Form = reduxForm({ - form: formName, -})(FormComponent); + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + ); + }} + /> +); export const Empty = { - component: Form, + component: FormComponent, props: { - onChange: ({ birthday }) => { - console.log('birthday changed to:', birthday ? birthday.toUTCString() : birthday); + onChange: formState => { + const birthday = formState.values.birthday; + if (birthday) { + console.log('birthday changed to:', birthday.toUTCString()); + } + }, + onSubmit: values => { + console.log('BirthdayInput.Form submitted values:', values); }, }, group: 'custom inputs', diff --git a/src/components/FieldBirthdayInput/FieldBirthdayInput.js b/src/components/FieldBirthdayInput/FieldBirthdayInput.js index 39e8519a..af978655 100644 --- a/src/components/FieldBirthdayInput/FieldBirthdayInput.js +++ b/src/components/FieldBirthdayInput/FieldBirthdayInput.js @@ -1,6 +1,10 @@ +/** + * NOTE this component is part of react-final-form instead of Redux Form. + */ + import React, { Component } from 'react'; import { func, instanceOf, object, node, string, bool } from 'prop-types'; -import { Field } from 'redux-form'; +import { Field } from 'react-final-form'; import { injectIntl, intlShape } from 'react-intl'; import classNames from 'classnames'; import { range } from 'lodash'; @@ -8,7 +12,7 @@ import { ValidationError } from '../../components'; import css from './FieldBirthdayInput.css'; -// Since redux-form tracks the onBlur event for marking the field as +// Since final-form tracks the onBlur event for marking the field as // touched (which triggers possible error validation rendering), only // trigger the event asynchronously when no other input within this // component has received focus. @@ -88,14 +92,14 @@ class BirthdayInputComponent extends Component { this.handleSelectChange = this.handleSelectChange.bind(this); } componentWillMount() { - const value = this.props.value; + const value = this.props.valueFromForm; if (value instanceof Date) { this.setState({ selected: selectedFromDate(value) }); } } componentWillReceiveProps(newProps) { - const oldValue = this.props.value; - const newValue = newProps.value; + const oldValue = this.props.valueFromForm; + const newValue = newProps.valueFromForm; const valueChanged = oldValue !== newValue; if (valueChanged && newValue instanceof Date) { this.setState({ selected: selectedFromDate(newValue) }); @@ -219,7 +223,7 @@ BirthdayInputComponent.defaultProps = { dateLabel: null, monthLabel: null, yearLabel: null, - value: null, + valueFromForm: null, disabled: false, }; @@ -231,7 +235,7 @@ BirthdayInputComponent.propTypes = { dateLabel: node, monthLabel: node, yearLabel: node, - value: instanceOf(Date), + valueFromForm: instanceOf(Date), onChange: func.isRequired, onFocus: func.isRequired, onBlur: func.isRequired, diff --git a/src/components/FieldBoolean/FieldBoolean.example.js b/src/components/FieldBoolean/FieldBoolean.example.js index fd7ccc91..818161b8 100644 --- a/src/components/FieldBoolean/FieldBoolean.example.js +++ b/src/components/FieldBoolean/FieldBoolean.example.js @@ -1,42 +1,50 @@ import React from 'react'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import * as validators from '../../util/validators'; import { Button } from '../../components'; import FieldBoolean from './FieldBoolean'; const formName = 'Styleguide.FieldBoolean.Form'; -const FormComponent = props => { - const { form, handleSubmit, invalid, pristine, submitting } = props; - const required = validators.requiredBoolean('This field is required'); - const submitDisabled = invalid || pristine || submitting; - return ( -
- - - - ); -}; - -FormComponent.propTypes = formPropTypes; - -const Form = reduxForm({ - form: formName, -})(FormComponent); +const FormComponent = props => ( + { + const { form, handleSubmit, onChange, invalid, pristine, submitting } = props; + const required = validators.requiredBoolean('This field is required'); + const submitDisabled = invalid || pristine || submitting; + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + ); + }} + /> +); export const YesNo = { - component: Form, + component: FormComponent, props: { - onChange(values) { - console.log('onChange:', values); + onChange: formState => { + if (Object.keys(formState.values).length > 0) { + console.log('form values changed to:', formState.values); + } }, onSubmit(values) { console.log('onSubmit:', values); diff --git a/src/components/FieldBoolean/FieldBoolean.js b/src/components/FieldBoolean/FieldBoolean.js index 63340ac5..3f19edcc 100644 --- a/src/components/FieldBoolean/FieldBoolean.js +++ b/src/components/FieldBoolean/FieldBoolean.js @@ -1,6 +1,6 @@ import React from 'react'; import { injectIntl, intlShape } from 'react-intl'; -import { SelectField } from '../../components'; +import { FieldSelect } from '../../components'; const FieldBoolean = props => { const { placeholder, intl, ...rest } = props; @@ -30,11 +30,11 @@ const FieldBoolean = props => { }, }; return ( - + - + ); }; diff --git a/src/components/FieldCheckbox/FieldCheckbox.example.js b/src/components/FieldCheckbox/FieldCheckbox.example.js index 5c1f88a8..e5009cdf 100644 --- a/src/components/FieldCheckbox/FieldCheckbox.example.js +++ b/src/components/FieldCheckbox/FieldCheckbox.example.js @@ -1,43 +1,47 @@ import React from 'react'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import { Button } from '../../components'; import FieldCheckbox from './FieldCheckbox'; -import { required } from '../../util/validators'; const formName = 'Styleguide.FieldCheckbox.Form'; -const FormComponent = props => { - const { form, handleSubmit, invalid, pristine, submitting } = props; +const FormComponent = props => ( + { + const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps; - const submitDisabled = invalid || pristine || submitting; + const submitDisabled = invalid || pristine || submitting; - return ( -
- + return ( + { + e.preventDefault(); + handleSubmit(e); + }} + > + + + - - - - - ); -}; - -FormComponent.propTypes = formPropTypes; - -const Form = reduxForm({ - form: formName, -})(FormComponent); + + + ); + }} + /> +); export const Checkbox = { - component: Form, + component: FormComponent, props: { + onChange: formState => { + if (Object.keys(formState.values).length > 0) { + console.log('form values changed to:', formState.values); + } + }, onSubmit: values => { console.log('Submit values of FieldCheckbox: ', values); }, diff --git a/src/components/FieldCheckbox/FieldCheckbox.js b/src/components/FieldCheckbox/FieldCheckbox.js index 13a94036..2084bbde 100644 --- a/src/components/FieldCheckbox/FieldCheckbox.js +++ b/src/components/FieldCheckbox/FieldCheckbox.js @@ -1,8 +1,11 @@ +/** + * NOTE this component is part of react-final-form instead of Redux Form. + */ + import React from 'react'; -import { any, node, string, object, shape } from 'prop-types'; +import { node, string } from 'prop-types'; import classNames from 'classnames'; -import { Field } from 'redux-form'; -import { ValidationError } from '../../components'; +import { Field } from 'react-final-form'; import css from './FieldCheckbox.css'; @@ -35,32 +38,26 @@ IconCheckbox.defaultProps = { className: null }; IconCheckbox.propTypes = { className: string }; const FieldCheckboxComponent = props => { - const { rootClassName, className, svgClassName, id, label, input, meta, ...rest } = props; + const { rootClassName, className, svgClassName, id, label, ...rest } = props; const classes = classNames(rootClassName || css.root, className); - - const { value, ...inputProps } = input; - const checked = value === true; - const checkboxProps = { id, className: css.input, + component: 'input', type: 'checkbox', - checked, - ...inputProps, ...rest, }; return ( - + - ); }; @@ -76,16 +73,16 @@ FieldCheckboxComponent.propTypes = { className: string, rootClassName: string, svgClassName: string, + + // Id is needed to connect the label with input. id: string.isRequired, label: node, - // redux-form Field params - input: shape({ value: any }).isRequired, - meta: object.isRequired, + // Name groups several checkboxes to an array of selected values + name: string.isRequired, + + // Checkbox needs a value that is passed forward when user checks the checkbox + value: string.isRequired, }; -const FieldCheckbox = props => { - return ; -}; - -export default FieldCheckbox; +export default FieldCheckboxComponent; diff --git a/src/components/FieldCheckboxGroup/FieldCheckboxGroup.css b/src/components/FieldCheckboxGroup/FieldCheckboxGroup.css new file mode 100644 index 00000000..0550ae37 --- /dev/null +++ b/src/components/FieldCheckboxGroup/FieldCheckboxGroup.css @@ -0,0 +1,28 @@ +@import '../../marketplace.css'; + +.root { + margin: 0; + padding: 0; + border: none; +} + +.list { + margin: 0; +} + +.twoColumns { + @media (--viewportMedium) { + columns: 2; + } +} + +.item { + padding: 2px 0; + + /* Fix broken multi-column layout in Chrome */ + page-break-inside: avoid; + + @media (--viewportMedium) { + padding: 4px 0; + } +} diff --git a/src/components/FieldCheckboxGroup/FieldCheckboxGroup.example.js b/src/components/FieldCheckboxGroup/FieldCheckboxGroup.example.js new file mode 100644 index 00000000..f7ce91f2 --- /dev/null +++ b/src/components/FieldCheckboxGroup/FieldCheckboxGroup.example.js @@ -0,0 +1,151 @@ +import React from 'react'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; +import arrayMutators from 'final-form-arrays'; +import { Button } from '../../components'; +import FieldCheckboxGroup from './FieldCheckboxGroup'; +import { requiredFieldArrayCheckbox } from '../../util/validators'; + +const formName = 'Styleguide.FieldCheckboxGroup'; +const formNameRequired = 'Styleguide.FieldCheckboxGroupRequired'; + +const label =

Amenities

; + +const commonProps = { + label: label, + options: [ + { + key: 'towels', + label: 'Towels', + }, + { + key: 'bathroom', + label: 'Bathroom', + }, + { + key: 'swimming_pool', + label: 'Swimming pool', + }, + { + key: 'own_drinks', + label: 'Own drinks allowed', + }, + { + key: 'jacuzzi', + label: 'Jacuzzi', + }, + { + key: 'audiovisual_entertainment', + label: 'Audiovisual entertainment', + }, + { + key: 'barbeque', + label: 'Barbeque', + }, + { + key: 'own_food_allowed', + label: 'Own food allowed', + }, + ], + twoColumns: true, +}; + +const optionalProps = { + name: 'amenities-optional', + id: 'amenities-optional', + ...commonProps, +}; + +const requiredProps = { + name: 'amenities-required', + id: `${formNameRequired}.amenities-required`, + ...commonProps, + validate: requiredFieldArrayCheckbox('this is required'), +}; +const tosProps = { + name: 'terms-of-service', + id: `${formNameRequired}.tos-accepted`, + options: [ + { + key: 'tos', + label: 'Terms of Service', + }, + ], + validate: requiredFieldArrayCheckbox('You need to accept Terms of Service'), +}; + +const formComponent = country => props => ( + { + const { handleSubmit, invalid, submitting, componentProps, onChange } = fieldRenderProps; + + const submitDisabled = invalid || submitting; + + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + + ); + }} + /> +); + +export const Optional = { + component: formComponent(formName), + props: { + onChange: formState => { + if (formState.dirty) { + console.log('form values changed to:', formState.values); + } + }, + onSubmit: values => { + console.log('Submit values: ', values); + }, + initialValues: { [optionalProps.name]: ['jacuzzi', 'towels'] }, + componentProps: optionalProps, + }, + group: 'inputs', +}; + +export const Required = { + component: formComponent(formNameRequired), + props: { + onChange: formState => { + if (formState.dirty) { + console.log('form values changed to:', formState.values); + } + }, + onSubmit: values => { + console.log('Submit values: ', values); + }, + componentProps: requiredProps, + }, + group: 'inputs', +}; + +export const ToSAccepted = { + component: formComponent(formNameRequired), + props: { + onChange: formState => { + if (formState.dirty) { + console.log('form values changed to:', formState.values); + } + }, + onSubmit: values => { + console.log('Submit values: ', values); + }, + componentProps: tosProps, + }, + group: 'inputs', +}; diff --git a/src/components/FieldCheckboxGroup/FieldCheckboxGroup.js b/src/components/FieldCheckboxGroup/FieldCheckboxGroup.js new file mode 100644 index 00000000..bd70f769 --- /dev/null +++ b/src/components/FieldCheckboxGroup/FieldCheckboxGroup.js @@ -0,0 +1,96 @@ +/** + * NOTE this component is part of react-final-form instead of Redux Form. + */ + +/* + * Renders a group of checkboxes that can be used to select + * multiple values from a set of options. + * + * The corresponding component when rendering the selected + * values is PropertyGroup. + * + */ + +import React, { Component } from 'react'; +import { arrayOf, bool, node, shape, string } from 'prop-types'; +import classNames from 'classnames'; +import { FieldArray } from 'react-final-form-arrays'; +import { FieldCheckbox, ValidationError } from '../../components'; + +import css from './FieldCheckboxGroup.css'; + +class FieldCheckboxRenderer extends Component { + constructor(props) { + super(props); + this.state = { touched: false }; + } + + componentWillReceiveProps(nextProps) { + // FieldArray doesn't have touched prop, so we'll keep track of it + if (!this.state.touched) { + this.setState({ touched: nextProps.meta.dirty }); + } + } + + render() { + const { className, rootClassName, label, twoColumns, id, fields, options, meta } = this.props; + + const touched = this.state.touched; + + const classes = classNames(rootClassName || css.root, className); + const listClasses = twoColumns ? classNames(css.list, css.twoColumns) : css.list; + + return ( +
+ {label ? {label} : null} +
    + {options.map((option, index) => { + const fieldId = `${id}.${option.key}`; + return ( +
  • + +
  • + ); + })} +
+ +
+ ); + } +} + +FieldCheckboxRenderer.defaultProps = { + rootClassName: null, + className: null, + label: null, + twoColumns: false, +}; + +FieldCheckboxRenderer.propTypes = { + rootClassName: string, + className: string, + id: string.isRequired, + label: node, + options: arrayOf( + shape({ + key: string.isRequired, + label: node.isRequired, + }) + ).isRequired, + twoColumns: bool, +}; + +const FieldCheckboxGroup = props => ; + +// Name and component are required fields for FieldArray. +// Component-prop we define in this file, name needs to be passed in +FieldCheckboxGroup.propTypes = { + name: string.isRequired, +}; + +export default FieldCheckboxGroup; diff --git a/src/components/FieldCurrencyInput/FieldCurrencyInput.example.js b/src/components/FieldCurrencyInput/FieldCurrencyInput.example.js index 78a18649..9f528f1d 100644 --- a/src/components/FieldCurrencyInput/FieldCurrencyInput.example.js +++ b/src/components/FieldCurrencyInput/FieldCurrencyInput.example.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import en from 'react-intl/locale-data/en'; import fi from 'react-intl/locale-data/fi'; import { currencyConfig } from '../../util/test-data'; @@ -81,36 +81,46 @@ export const defaultValueWithFiEUR = { group: 'custom inputs', }; -const formName = 'Styleguide.FieldCurrencyInput.Form'; +const FormComponent = props => ( + { + const { handleSubmit, onChange } = fieldRenderProps; + const required = validators.required('This field is required'); -const FormComponent = props => { - const { form } = props; - const required = validators.required('This field is required'); - return ( -
- - - ); -}; - -FormComponent.propTypes = formPropTypes; - -const Form = reduxForm({ - form: formName, -})(FormComponent); + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + ); + }} + /> +); export const FieldInForm = { - component: Form, + component: FormComponent, props: { - onChange: values => { - console.log('form values changed to:', values); + onChange: formState => { + if (formState.values && formState.values.price) { + console.log('form values changed to:', formState.values); + } + }, + onSubmit: values => { + console.log('FieldInForm submitted values:', values); + return false; }, }, group: 'custom inputs', diff --git a/src/components/FieldCurrencyInput/FieldCurrencyInput.js b/src/components/FieldCurrencyInput/FieldCurrencyInput.js index 2f3614d0..4893253a 100644 --- a/src/components/FieldCurrencyInput/FieldCurrencyInput.js +++ b/src/components/FieldCurrencyInput/FieldCurrencyInput.js @@ -1,3 +1,7 @@ +/** + * NOTE this component is part of react-final-form instead of Redux Form. + */ + /** * CurrencyInput renders an input field that format it's value according to currency formatting rules * onFocus: renders given value in unformatted manner: "9999,99" @@ -6,7 +10,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { intlShape, injectIntl } from 'react-intl'; -import { Field } from 'redux-form'; +import { Field } from 'react-final-form'; import classNames from 'classnames'; import Decimal from 'decimal.js'; import { ValidationError } from '../../components'; @@ -279,7 +283,7 @@ FieldCurrencyInputComponent.propTypes = { id: string, label: string, - // Generated by redux-form's Field component + // Generated by final-form's Field component input: object.isRequired, meta: object.isRequired, }; diff --git a/src/components/FieldDateInput/DateInput.js b/src/components/FieldDateInput/DateInput.js index 6d124faa..5b51dc51 100644 --- a/src/components/FieldDateInput/DateInput.js +++ b/src/components/FieldDateInput/DateInput.js @@ -46,6 +46,8 @@ const defaultProps = { horizontalMargin: 0, withPortal: false, withFullScreenPortal: false, + appendToBody: false, + disableScroll: false, initialVisibleMonth: null, firstDayOfWeek: config.i18n.firstDayOfWeek, numberOfMonths: 1, @@ -137,12 +139,12 @@ class DateInputComponent extends Component { onBlur, onChange, onFocus, - onDragStart, - onDrop, phrases, screenReaderInputMessage, useMobileMargins, value, + children, + render, ...datePickerProps } = this.props; /* eslint-enable no-unused-vars */ @@ -194,6 +196,7 @@ DateInputComponent.defaultProps = { DateInputComponent.propTypes = { className: string, + id: string, focused: bool, initialDate: instanceOf(Date), intl: intlShape.isRequired, @@ -202,8 +205,6 @@ DateInputComponent.propTypes = { onChange: func.isRequired, onBlur: func.isRequired, onFocus: func.isRequired, - onDragStart: func.isRequired, - onDrop: func.isRequired, phrases: shape({ closeDatePicker: string, clearDate: string, diff --git a/src/components/FieldDateInput/FieldDateInput.example.js b/src/components/FieldDateInput/FieldDateInput.example.js index c0f57ef2..11d37741 100644 --- a/src/components/FieldDateInput/FieldDateInput.example.js +++ b/src/components/FieldDateInput/FieldDateInput.example.js @@ -1,34 +1,51 @@ /* eslint-disable no-console */ import React from 'react'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import moment from 'moment'; import { Button } from '../../components'; -import { required, bookingDateRequired } from '../../util/validators'; +import { required, bookingDateRequired, composeValidators } from '../../util/validators'; import FieldDateInput from './FieldDateInput'; -const FormComponent = props => { - const { form, handleSubmit, pristine, submitting, dateInputProps } = props; - const submitDisabled = pristine || submitting; - return ( -
- - - - ); -}; +const FormComponent = props => ( + { + const { + form, + handleSubmit, + onChange, + pristine, + submitting, + dateInputProps, + values, + } = fieldRenderProps; + const submitDisabled = pristine || submitting; + if (values && values.bookingDates) { + onChange(values.bookingDates); + } -FormComponent.propTypes = formPropTypes; + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + ); + }} + /> +); -const defaultFormName = 'Styleguide.DateInput.Form'; - -const Form = reduxForm({ - form: defaultFormName, -})(FormComponent); +const defaultFormName = 'FieldDateInputExampleForm'; export const Empty = { - component: Form, + component: FormComponent, props: { dateInputProps: { name: 'bookingDate', @@ -37,13 +54,15 @@ export const Empty = { label: 'Date', placeholderText: moment().format('ddd, MMMM D'), format: null, - validate: [required('Required'), bookingDateRequired('Date is not valid')], + validate: composeValidators(required('Required'), bookingDateRequired('Date is not valid')), onBlur: () => console.log('onBlur called from DateInput props.'), onFocus: () => console.log('onFocus called from DateInput props.'), }, - onChange: values => { - const { date } = values; - console.log('Changed to', moment(date).format('L')); + onChange: formState => { + const { date } = formState.values; + if (date) { + console.log('Changed to', moment(date).format('L')); + } }, onSubmit: values => { console.log('Submitting a form with values:', values); diff --git a/src/components/FieldDateInput/FieldDateInput.js b/src/components/FieldDateInput/FieldDateInput.js index 6be23eb1..a1b3f9e5 100644 --- a/src/components/FieldDateInput/FieldDateInput.js +++ b/src/components/FieldDateInput/FieldDateInput.js @@ -1,12 +1,12 @@ /** - * Provides a date picker for Redux Forms (using https://github.com/airbnb/react-dates) + * Provides a date picker for Final Forms (using https://github.com/airbnb/react-dates) * * NOTE: If you are using this component inside BookingDatesForm, * you should convert value.date to start date and end date before submitting it to API */ import React, { Component } from 'react'; import { bool, object, string } from 'prop-types'; -import { Field } from 'redux-form'; +import { Field } from 'react-final-form'; import classNames from 'classnames'; import { ValidationError } from '../../components'; @@ -49,6 +49,7 @@ class FieldDateInputComponent extends Component { onBlur: input.onBlur, onFocus: input.onFocus, useMobileMargins, + id, ...restOfInput, ...rest, }; diff --git a/src/components/FieldDateInput/FieldDateInput.test.js b/src/components/FieldDateInput/FieldDateInput.test.js index cb4ee8a0..cd18f409 100644 --- a/src/components/FieldDateInput/FieldDateInput.test.js +++ b/src/components/FieldDateInput/FieldDateInput.test.js @@ -16,8 +16,6 @@ describe('DateInput', () => { onBlur: noop, onChange: noop, onFocus: noop, - onDragStart: noop, - onDrop: noop, id: 'bookingDate', placeholderText: 'today', }; diff --git a/src/components/FieldDateRangeInput/DateRangeInput.js b/src/components/FieldDateRangeInput/DateRangeInput.js index 4edc5ce6..de0c5e13 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.js @@ -21,7 +21,7 @@ import css from './DateRangeInput.css'; export const HORIZONTAL_ORIENTATION = 'horizontal'; export const ANCHOR_LEFT = 'left'; -// Since redux-form tracks the onBlur event for marking the field as +// Since final-form tracks the onBlur event for marking the field as // touched (which triggers possible error validation rendering), only // trigger the event asynchronously when no other input within this // component has received focus. @@ -89,6 +89,8 @@ const defaultProps = { horizontalMargin: 0, withPortal: false, withFullScreenPortal: false, + appendToBody: false, + disableScroll: false, daySize: 38, isRTL: false, initialVisibleMonth: null, @@ -202,12 +204,12 @@ class DateRangeInputComponent extends Component { onBlur, onChange, onFocus, - onDragStart, - onDrop, phrases, screenReaderInputMessage, useMobileMargins, value, + children, + render, ...datePickerProps } = this.props; /* eslint-enable no-unused-vars */ @@ -268,6 +270,8 @@ DateRangeInputComponent.defaultProps = { DateRangeInputComponent.propTypes = { className: string, + startDateId: string, + endDateId: string, unitType: propTypes.bookingUnitType.isRequired, focusedInput: oneOf([START_DATE, END_DATE]), initialDates: instanceOf(Date), @@ -277,8 +281,6 @@ DateRangeInputComponent.propTypes = { onChange: func.isRequired, onBlur: func.isRequired, onFocus: func.isRequired, - onDragStart: func.isRequired, - onDrop: func.isRequired, phrases: shape({ closeDatePicker: string, clearDate: string, diff --git a/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js b/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js index 9a95d5c9..4a8bcecb 100644 --- a/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js +++ b/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js @@ -1,35 +1,48 @@ /* eslint-disable no-console */ import React from 'react'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import moment from 'moment'; import { Button } from '../../components'; -import { required, bookingDatesRequired } from '../../util/validators'; +import { required, bookingDatesRequired, composeValidators } from '../../util/validators'; import { LINE_ITEM_NIGHT } from '../../util/types'; import FieldDateRangeInput from './FieldDateRangeInput'; -const FormComponent = props => { - const { form, handleSubmit, pristine, submitting, dateInputProps } = props; - const submitDisabled = pristine || submitting; - return ( -
- - - - ); -}; +const FormComponent = props => ( + { + const { + form, + handleSubmit, + onChange, + pristine, + submitting, + dateInputProps, + } = fieldRenderProps; + const submitDisabled = pristine || submitting; -FormComponent.propTypes = formPropTypes; + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + ); + }} + /> +); -const defaultFormName = 'Styleguide.DateRangeInput.Form'; - -const Form = reduxForm({ - form: defaultFormName, -})(FormComponent); +const defaultFormName = 'FieldDateRangeInputExampleForm'; export const Empty = { - component: Form, + component: FormComponent, props: { dateInputProps: { name: 'bookingDates', @@ -43,16 +56,18 @@ export const Empty = { .add(1, 'days') .format('ddd, MMMM D'), format: null, - validate: [ + validate: composeValidators( required('Required'), - bookingDatesRequired('Start date is not valid', 'End date is not valid'), - ], + bookingDatesRequired('Start date is not valid', 'End date is not valid') + ), onBlur: () => console.log('onBlur called from DateRangeInput props.'), onFocus: () => console.log('onFocus called from DateRangeInput props.'), }, - onChange: values => { - const { startDate, endDate } = values; - console.log('Changed to', moment(startDate).format('L'), moment(endDate).format('L')); + onChange: formState => { + const { startDate, endDate } = formState.values; + if (startDate || endDate) { + console.log('Changed to', moment(startDate).format('L'), moment(endDate).format('L')); + } }, onSubmit: values => { console.log('Submitting a form with values:', values); diff --git a/src/components/FieldDateRangeInput/FieldDateRangeInput.js b/src/components/FieldDateRangeInput/FieldDateRangeInput.js index 087cebf4..17b0c07e 100644 --- a/src/components/FieldDateRangeInput/FieldDateRangeInput.js +++ b/src/components/FieldDateRangeInput/FieldDateRangeInput.js @@ -1,6 +1,13 @@ +/** + * Provides a date picker for Final Forms (using https://github.com/airbnb/react-dates) + * + * NOTE: If you are using this component inside BookingDatesForm, + * you should convert value.date to start date and end date before submitting it to API + */ + import React, { Component } from 'react'; import { bool, func, object, oneOf, string } from 'prop-types'; -import { Field } from 'redux-form'; +import { Field } from 'react-final-form'; import classNames from 'classnames'; import { START_DATE, END_DATE } from '../../util/dates'; import { propTypes } from '../../util/types'; @@ -117,6 +124,8 @@ class FieldDateRangeInputComponent extends Component { ...restOfInput, ...rest, focusedInput: this.state.focusedInput, + startDateId, + endDateId, }; const classes = classNames(rootClassName || css.fieldRoot, className); const errorClasses = classNames({ [css.mobileMargins]: useMobileMargins }); diff --git a/src/components/FieldDateRangeInput/FieldDateRangeInput.test.js b/src/components/FieldDateRangeInput/FieldDateRangeInput.test.js index c63903f4..bc2abaf1 100644 --- a/src/components/FieldDateRangeInput/FieldDateRangeInput.test.js +++ b/src/components/FieldDateRangeInput/FieldDateRangeInput.test.js @@ -18,8 +18,6 @@ describe('DateRangeInput', () => { onBlur: noop, onChange: noop, onFocus: noop, - onDragStart: noop, - onDrop: noop, startDateId: 'bookingStartDate', startDatePlaceholderText: 'today', endDateId: 'bookingEndDate', diff --git a/src/components/FieldGroupCheckbox/FieldCheckbox.css b/src/components/FieldGroupCheckbox/FieldCheckbox.css new file mode 100644 index 00000000..a295dbf0 --- /dev/null +++ b/src/components/FieldGroupCheckbox/FieldCheckbox.css @@ -0,0 +1,68 @@ +@import '../../marketplace.css'; + +.root { + position: relative; +} + +.input { + position: absolute; + opacity: 0; + height: 0; + width: 0; + + /* Highlight the borders if the checkbox is hovered, focused or checked */ + &:hover + label .box, + &:focus + label .box, + &:checked + label .box { + stroke: var(--marketplaceColor); + } + + /* Display the "check" when checked */ + &:checked + label .checked { + display: inline; + stroke: var(--marketplaceColor); + stroke-width: 1px; + } + + /* Hightlight the text on checked, hover and focus */ + &:focus + label .text, + &:hover + label .text, + &:checked + label .text { + color: var(--matterColorDark); + } +} + +.label { + display: flex; + align-items: center; + padding: 0; +} + +.checkboxWrapper { + /* This should follow line-height */ + height: 32px; + margin-top: -1px; + margin-right: 12px; + align-self: baseline; + + display: inline-flex; + align-items: center; + cursor: pointer; +} + +.checked { + display: none; + fill: var(--marketplaceColor); +} + +.box { + stroke: var(--matterColorAnti); +} + +.text { + @apply --marketplaceListingAttributeFontStyles; + color: var(--matterColor); + margin-top: -1px; + margin-bottom: 1px; + cursor: pointer; +} diff --git a/src/components/FieldGroupCheckbox/FieldCheckbox.js b/src/components/FieldGroupCheckbox/FieldCheckbox.js new file mode 100644 index 00000000..13a94036 --- /dev/null +++ b/src/components/FieldGroupCheckbox/FieldCheckbox.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { any, node, string, object, shape } from 'prop-types'; +import classNames from 'classnames'; +import { Field } from 'redux-form'; +import { ValidationError } from '../../components'; + +import css from './FieldCheckbox.css'; + +const IconCheckbox = props => { + return ( + + + + + + + + + + ); +}; + +IconCheckbox.defaultProps = { className: null }; + +IconCheckbox.propTypes = { className: string }; + +const FieldCheckboxComponent = props => { + const { rootClassName, className, svgClassName, id, label, input, meta, ...rest } = props; + + const classes = classNames(rootClassName || css.root, className); + + const { value, ...inputProps } = input; + const checked = value === true; + + const checkboxProps = { + id, + className: css.input, + type: 'checkbox', + checked, + ...inputProps, + ...rest, + }; + + return ( + + + + + + ); +}; + +FieldCheckboxComponent.defaultProps = { + className: null, + rootClassName: null, + svgClassName: null, + label: null, +}; + +FieldCheckboxComponent.propTypes = { + className: string, + rootClassName: string, + svgClassName: string, + id: string.isRequired, + label: node, + + // redux-form Field params + input: shape({ value: any }).isRequired, + meta: object.isRequired, +}; + +const FieldCheckbox = props => { + return ; +}; + +export default FieldCheckbox; diff --git a/src/components/FieldGroupCheckbox/FieldGroupCheckbox.js b/src/components/FieldGroupCheckbox/FieldGroupCheckbox.js index 39f80445..14876e96 100644 --- a/src/components/FieldGroupCheckbox/FieldGroupCheckbox.js +++ b/src/components/FieldGroupCheckbox/FieldGroupCheckbox.js @@ -11,8 +11,9 @@ import React, { Component } from 'react'; import { arrayOf, bool, node, shape, string } from 'prop-types'; import classNames from 'classnames'; import { FieldArray } from 'redux-form'; -import { FieldCheckbox, ValidationError } from '../../components'; +import { ValidationError } from '../../components'; +import FieldCheckbox from './FieldCheckbox'; import css from './FieldGroupCheckbox.css'; class FieldCheckboxRenderer extends Component { @@ -29,8 +30,7 @@ class FieldCheckboxRenderer extends Component { } render() { - const { className, rootClassName, label, twoColumns, id, options, fields, meta } = this.props; - const name = fields.name; + const { className, rootClassName, label, twoColumns, id, options, meta, name } = this.props; const touched = this.state.touched; @@ -77,7 +77,11 @@ FieldCheckboxRenderer.propTypes = { twoColumns: bool, }; -const FieldGroupCheckbox = props => ; +// Redux Form: the name of FieldArray must be unique +// https://github.com/erikras/redux-form/issues/2740 +const FieldGroupCheckbox = props => ( + +); // Name and component are required fields for FieldArray. // Component-prop we define in this file, name needs to be passed in diff --git a/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.example.js b/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.example.js index 134a9aa2..d27c0999 100644 --- a/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.example.js +++ b/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.example.js @@ -1,40 +1,52 @@ import React from 'react'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import * as validators from '../../util/validators'; import { Button } from '../../components'; import FieldPhoneNumberInput from './FieldPhoneNumberInput'; const formName = 'Styleguide.FieldPhoneNumberInput.Form'; -const FormComponent = props => { - const { form, handleSubmit, invalid, pristine, submitting } = props; - const required = validators.required('This field is required'); - const submitDisabled = invalid || pristine || submitting; - return ( -
- - - - ); -}; - -FormComponent.propTypes = formPropTypes; - -const Form = reduxForm({ - form: formName, -})(FormComponent); +const FormComponent = props => ( + { + const { formId, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps; + const required = validators.required('This field is required'); + const submitDisabled = invalid || pristine || submitting; + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + ); + }} + /> +); export const PhoneNumber = { - component: Form, + component: FormComponent, props: { + formId: 'PhoneNumberExample', + onChange: formState => { + if (formState.dirty) { + console.log('form values changed to:', formState.values); + } + }, onSubmit(values) { console.log('onSubmit:', values); }, diff --git a/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.js b/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.js index 82c00103..6a44b4a5 100644 --- a/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.js +++ b/src/components/FieldPhoneNumberInput/FieldPhoneNumberInput.js @@ -1,3 +1,7 @@ +/** + * DEPRECATED: this component is part of Redux Form - we are migrating to react-final-form. + */ + /** * A text field with phone number formatting. By default uses formatting * rules defined in the fiFormatter.js file. To change the formatting @@ -6,7 +10,7 @@ */ import React from 'react'; -import { TextInputField } from '../../components'; +import { FieldTextInput } from '../../components'; import { format, parse } from './fiFormatter'; const FieldPhoneNumberInput = props => { @@ -17,7 +21,7 @@ const FieldPhoneNumberInput = props => { parse: parse, }; - return ; + return ; }; export default FieldPhoneNumberInput; diff --git a/src/components/FieldReviewRating/FieldReviewRating.example.js b/src/components/FieldReviewRating/FieldReviewRating.example.js index 53512eb0..11a0cbbe 100644 --- a/src/components/FieldReviewRating/FieldReviewRating.example.js +++ b/src/components/FieldReviewRating/FieldReviewRating.example.js @@ -1,41 +1,52 @@ /* eslint-disable no-console */ import React from 'react'; -import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; import * as validators from '../../util/validators'; import { Button } from '../../components'; import FieldReviewRating from './FieldReviewRating'; const formName = 'Styleguide.FieldReviewRating.Form'; -const FormComponent = props => { - const { form, handleSubmit, invalid, pristine, submitting } = props; - const required = validators.required('This field is required'); - const submitDisabled = invalid || pristine || submitting; +const FormComponent = props => ( + { + const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps; + const required = validators.required('This field is required'); + const submitDisabled = invalid || pristine || submitting; - return ( -
- - - - ); -}; - -FormComponent.propTypes = formPropTypes; - -const Form = reduxForm({ - form: formName, -})(FormComponent); + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + ); + }} + /> +); export const StarRating = { - component: Form, + component: FormComponent, props: { + onChange: formState => { + if (formState.dirty) { + console.log('form values changed to:', formState.values); + } + }, onSubmit: values => { console.log('submit values:', values); }, diff --git a/src/components/FieldReviewRating/FieldReviewRating.js b/src/components/FieldReviewRating/FieldReviewRating.js index 68ef0c6f..72b4f34a 100644 --- a/src/components/FieldReviewRating/FieldReviewRating.js +++ b/src/components/FieldReviewRating/FieldReviewRating.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { intlShape, injectIntl } from 'react-intl'; -import { Field } from 'redux-form'; +import { Field } from 'react-final-form'; import classNames from 'classnames'; import { IconReviewStar, ValidationError } from '../../components'; @@ -13,12 +13,6 @@ class FieldReviewRatingComponent extends Component { this.handleChange = this.handleChange.bind(this); } - componentWillUnmount() { - if (this.props.clearOnUnmount) { - this.props.input.onChange(''); - } - } - handleChange(event) { this.props.input.onChange(event.target.value); } @@ -29,7 +23,6 @@ class FieldReviewRatingComponent extends Component { rootClassName, className, inputRootClass, - clearOnUnmount, customErrorText, id, intl, @@ -105,17 +98,15 @@ class FieldReviewRatingComponent extends Component { FieldReviewRatingComponent.defaultProps = { rootClassName: null, className: null, - clearOnUnmount: false, customErrorText: null, label: null, }; -const { string, bool, shape, func, object } = PropTypes; +const { string, shape, func, object } = PropTypes; FieldReviewRatingComponent.propTypes = { rootClassName: string, className: string, - clearOnUnmount: bool, id: string.isRequired, label: string, diff --git a/src/components/FieldSelect/FieldSelect.css b/src/components/FieldSelect/FieldSelect.css new file mode 100644 index 00000000..0aa58c74 --- /dev/null +++ b/src/components/FieldSelect/FieldSelect.css @@ -0,0 +1,18 @@ +@import '../../marketplace.css'; + +.root { +} + +.select { + color: var(--matterColorAnti); + border-bottom-color: var(--attentionColor); +} + +.selectSuccess { + color: var(--matterColor); + border-bottom-color: var(--successColor); +} + +.selectError { + border-bottom-color: var(--failColor); +} diff --git a/src/components/FieldSelect/FieldSelect.example.js b/src/components/FieldSelect/FieldSelect.example.js new file mode 100644 index 00000000..de6fae6a --- /dev/null +++ b/src/components/FieldSelect/FieldSelect.example.js @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +import React from 'react'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; +import * as validators from '../../util/validators'; +import { Button } from '../../components'; +import FieldSelect from './FieldSelect'; + +const FormComponent = props => ( + { + const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps; + const required = validators.required('This field is required'); + const submitDisabled = invalid || pristine || submitting; + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + + + + + ); + }} + /> +); + +export const Select = { + component: FormComponent, + props: { + onChange: formState => { + if (formState.values.select1) { + console.log('form values changed to:', formState.values); + } + }, + onSubmit: values => { + console.log('submit values:', values); + return false; + }, + }, + group: 'inputs', +}; diff --git a/src/components/FieldSelect/FieldSelect.js b/src/components/FieldSelect/FieldSelect.js new file mode 100644 index 00000000..dd13589d --- /dev/null +++ b/src/components/FieldSelect/FieldSelect.js @@ -0,0 +1,72 @@ +/** + * NOTE this component is part of react-final-form instead of Redux Form. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field } from 'react-final-form'; +import classNames from 'classnames'; +import { ValidationError } from '../../components'; + +import css from './FieldSelect.css'; + +const FieldSelectComponent = props => { + const { rootClassName, className, id, label, input, meta, children, ...rest } = props; + + if (label && !id) { + throw new Error('id required when a label is given'); + } + + const { valid, invalid, touched, error } = meta; + + // Error message and input error styles are only shown if the + // field has been touched and the validation has failed. + const hasError = touched && invalid && error; + + const selectClasses = classNames(css.select, { + [css.selectSuccess]: valid, + [css.selectError]: hasError, + }); + const selectProps = { className: selectClasses, id, ...input, ...rest }; + + const classes = classNames(rootClassName || css.root, className); + return ( +
+ {label ? : null} + + +
+ ); +}; + +FieldSelectComponent.defaultProps = { + rootClassName: null, + className: null, + id: null, + label: null, + children: null, +}; + +const { string, object, node } = PropTypes; + +FieldSelectComponent.propTypes = { + rootClassName: string, + className: string, + + // Label is optional, but if it is given, an id is also required so + // the label can reference the input in the `for` attribute + id: string, + label: string, + + // Generated by final-form's Field component + input: object.isRequired, + meta: object.isRequired, + + children: node, +}; + +const FieldSelect = props => { + return ; +}; + +export default FieldSelect; diff --git a/src/components/FieldTextInput/FieldTextInput.css b/src/components/FieldTextInput/FieldTextInput.css new file mode 100644 index 00000000..2294a88d --- /dev/null +++ b/src/components/FieldTextInput/FieldTextInput.css @@ -0,0 +1,19 @@ +@import '../../marketplace.css'; + +.root { +} + +.input { + border-bottom-color: var(--attentionColor); +} + +.inputSuccess { + border-bottom-color: var(--successColor); +} + +.inputError { + border-bottom-color: var(--failColor); +} + +.textarea { +} diff --git a/src/components/FieldTextInput/FieldTextInput.example.css b/src/components/FieldTextInput/FieldTextInput.example.css new file mode 100644 index 00000000..9fe2d13d --- /dev/null +++ b/src/components/FieldTextInput/FieldTextInput.example.css @@ -0,0 +1,7 @@ +.field { + margin-top: 24px; +} + +.submit { + margin-top: 24px; +} diff --git a/src/components/FieldTextInput/FieldTextInput.example.js b/src/components/FieldTextInput/FieldTextInput.example.js new file mode 100644 index 00000000..68cd4e2e --- /dev/null +++ b/src/components/FieldTextInput/FieldTextInput.example.js @@ -0,0 +1,90 @@ +/* eslint-disable no-console */ +import React from 'react'; +import { Form as FinalForm, FormSpy } from 'react-final-form'; +import * as validators from '../../util/validators'; +import { Button } from '../../components'; +import FieldTextInput from './FieldTextInput'; + +import css from './FieldTextInput.example.css'; + +const FormComponent = props => ( + { + const { handleSubmit, onChange, invalid, pristine, submitting, formName } = fieldRenderProps; + const required = validators.required('This field is required'); + const submitDisabled = invalid || pristine || submitting; + return ( +
{ + e.preventDefault(); + handleSubmit(e); + }} + > + + + + + + + + + + ); + }} + /> +); + +export const Inputs = { + component: FormComponent, + props: { + formName: 'Inputs', + onChange: formState => { + if (Object.keys(formState.values).length > 0) { + console.log('form values changed to:', formState.values); + } + }, + onSubmit: values => { + console.log('submit values:', values); + }, + }, + group: 'inputs', +}; diff --git a/src/components/FieldTextInput/FieldTextInput.js b/src/components/FieldTextInput/FieldTextInput.js new file mode 100644 index 00000000..9af3262c --- /dev/null +++ b/src/components/FieldTextInput/FieldTextInput.js @@ -0,0 +1,121 @@ +/** + * NOTE this component is part of react-final-form instead of Redux Form. + */ + +import React, { Component } from 'react'; +import { func, object, shape, string } from 'prop-types'; +import { Field } from 'react-final-form'; +import classNames from 'classnames'; +import { ValidationError, ExpandingTextarea } from '../../components'; + +import css from './FieldTextInput.css'; + +const CONTENT_MAX_LENGTH = 5000; + +class FieldTextInputComponent extends Component { + render() { + /* eslint-disable no-unused-vars */ + const { + rootClassName, + className, + inputRootClass, + customErrorText, + id, + label, + type, + input, + meta, + onUnmount, + ...rest + } = this.props; + /* eslint-enable no-unused-vars */ + + if (label && !id) { + throw new Error('id required when a label is given'); + } + + const { valid, invalid, touched, error } = meta; + const isTextarea = type === 'textarea'; + + const errorText = customErrorText || error; + + // Error message and input error styles are only shown if the + // field has been touched and the validation has failed. + const hasError = !!customErrorText || !!(touched && invalid && error); + + const fieldMeta = { touched: hasError, error: errorText }; + + const inputClasses = + inputRootClass || + classNames(css.input, { + [css.inputSuccess]: valid, + [css.inputError]: hasError, + [css.textarea]: isTextarea, + }); + const inputProps = isTextarea + ? { className: inputClasses, id, rows: 1, maxLength: CONTENT_MAX_LENGTH, ...input, ...rest } + : { className: inputClasses, id, type, ...input, ...rest }; + + const classes = classNames(rootClassName || css.root, className); + return ( +
+ {label ? : null} + {isTextarea ? : } + +
+ ); + } +} + +FieldTextInputComponent.defaultProps = { + rootClassName: null, + className: null, + inputRootClass: null, + onUnmount: null, + customErrorText: null, + id: null, + label: null, +}; + +FieldTextInputComponent.propTypes = { + rootClassName: string, + className: string, + inputRootClass: string, + + onUnmount: func, + + // Error message that can be manually passed to input field, + // overrides default validation message + customErrorText: string, + + // Label is optional, but if it is given, an id is also required so + // the label can reference the input in the `for` attribute + id: string, + label: string, + + // Either 'textarea' or something that is passed to the input element + type: string.isRequired, + + // Generated by redux-form's Field component + input: shape({ + onChange: func.isRequired, + }).isRequired, + meta: object.isRequired, +}; + +class FieldTextInput extends Component { + componentWillUnmount() { + // Unmounting happens too late if it is done inside Field component + // (Then Form has already registered its (new) fields and + // changing the value without corresponding field is prohibited in Final Form + if (this.props.onUnmount) { + this.props.onUnmount(); + } + } + + render() { + return ; + } +} + +export default FieldTextInput; diff --git a/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js b/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js index 8bca821b..9a628118 100644 --- a/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js +++ b/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Field } from 'redux-form'; +import { Field } from 'react-final-form'; import { debounce } from 'lodash'; import classNames from 'classnames'; import { propTypes } from '../../util/types'; @@ -494,18 +494,14 @@ LocationAutocompleteInput.propTypes = { export default LocationAutocompleteInput; class LocationAutocompleteInputFieldComponent extends Component { - componentWillUnmount() { - if (this.props.clearOnUnmount) { - this.props.input.onChange(''); - } - } - render() { /* eslint-disable no-unused-vars */ - const { rootClassName, labelClassName, clearOnUnmount, ...restProps } = this.props; - const { input, label, meta, ...otherProps } = restProps; + const { rootClassName, labelClassName, ...restProps } = this.props; + const { input, label, meta, valueFromForm, ...otherProps } = restProps; /* eslint-enable no-unused-vars */ + const value = typeof valueFromForm !== 'undefined' ? valueFromForm : input.value; + const locationAutocompleteProps = { label, meta, ...otherProps, input: { ...input, value } }; const labelInfo = label ? (