Merge pull request #210 from sharetribe/use-input-field-in-forms

Use InputField in forms
This commit is contained in:
Kimmo Puputti 2017-06-07 14:47:43 +03:00 committed by GitHub
commit 1dbbc4e66c
10 changed files with 174 additions and 145 deletions

View file

@ -1,30 +1,26 @@
/* eslint-disable no-console */
import React, { Component } from 'react';
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import * as validators from '../../util/validators';
import { enhancedField } from '../../util/forms';
import { InputField } from '../../components';
import BirthdayInput from './BirthdayInput';
class FormComponent extends Component {
constructor(props) {
super(props);
this.EnhancedBirthdayInput = enhancedField(BirthdayInput);
}
render() {
const required = validators.required('A valid date is required');
return (
<form>
<Field
name="birthday"
label="Date of birth"
format={null}
component={this.EnhancedBirthdayInput}
validate={required}
/>
</form>
);
}
}
const FormComponent = () => {
const required = validators.required('A valid date is required');
return (
<form>
<Field
name="birthday"
label="Date of birth"
format={null}
type="custom"
inputComponent={BirthdayInput}
component={InputField}
validate={required}
/>
</form>
);
};
const Form = reduxForm({
form: 'Styleguide.BirthdayInput.Form',

View file

@ -85,14 +85,14 @@ class BirthdayInput extends Component {
this.handleSelectChange = this.handleSelectChange.bind(this);
}
componentWillMount() {
const value = this.props.input.value;
const value = this.props.value;
if (value instanceof Date) {
this.setState({ selected: selectedFromDate(value) });
}
}
componentWillReceiveProps(newProps) {
const oldValue = this.props.input.value;
const newValue = newProps.input.value;
const oldValue = this.props.value;
const newValue = newProps.value;
const valueChanged = oldValue !== newValue;
if (valueChanged && newValue instanceof Date) {
this.setState({ selected: selectedFromDate(newValue) });
@ -103,13 +103,13 @@ class BirthdayInput extends Component {
}
handleSelectFocus() {
window.clearTimeout(this.blurTimeoutId);
this.props.input.onFocus();
this.props.onFocus();
}
handleSelectBlur() {
window.clearTimeout(this.blurTimeoutId);
this.blurTimeoutId = window.setTimeout(
() => {
this.props.input.onBlur();
this.props.onBlur();
},
BLUR_TIMEOUT
);
@ -117,18 +117,18 @@ class BirthdayInput extends Component {
handleSelectChange(type, value) {
this.setState(prevState => {
const selected = { ...prevState.selected, [type]: parseNum(value) };
this.props.input.onChange(dateFromSelected(selected));
this.props.onChange(dateFromSelected(selected));
return { selected };
});
}
render() {
const { className } = this.props;
const { rootClassName, className } = this.props;
const selectedValue = n => {
return typeof n === 'number' ? n : '';
};
const classes = classNames(css.root, className);
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
@ -167,19 +167,21 @@ class BirthdayInput extends Component {
}
}
BirthdayInput.defaultProps = { className: '' };
BirthdayInput.defaultProps = {
rootClassName: null,
className: null,
value: null,
};
const { string, shape, instanceOf, func } = PropTypes;
const { string, instanceOf, func } = PropTypes;
BirthdayInput.propTypes = {
rootClassName: string,
className: string,
input: shape({
name: string.isRequired,
value: instanceOf(Date),
onChange: func.isRequired,
onFocus: func.isRequired,
onBlur: func.isRequired,
}).isRequired,
value: instanceOf(Date),
onChange: func.isRequired,
onFocus: func.isRequired,
onBlur: func.isRequired,
};
export default BirthdayInput;

View file

@ -1,10 +1,10 @@
@import '../../marketplace.css';
.root {
margin-top: calc(6 * var(--spacingUnit));
margin-top: calc(4 * var(--spacingUnit));
@media (--desktopViewport) {
margin-top: calc(6 * var(--spacingUnitDesktop));
margin-top: calc(4 * var(--spacingUnitDesktop));
}
}

View file

@ -39,6 +39,14 @@ const FormComponent = props => {
label="Label for input with initial value"
component={InputField}
/>
<Field
name="textarea1"
type="textarea"
label="Label for textarea"
placeholder="Textarea placeholder..."
validate={required}
component={InputField}
/>
<Button type="submit" disabled={submitDisabled} style={buttonStyles}>Submit form</Button>
</form>
);

View file

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react';
import { omit } from 'lodash';
import classNames from 'classnames';
import css from './InputField.css';
@ -16,6 +17,7 @@ class InputField extends Component {
labelRootClassName,
inputRootClassName,
errorRootClassName,
inputComponent: InputComponent,
type,
label,
placeholder,
@ -23,29 +25,50 @@ class InputField extends Component {
input,
meta,
} = this.props;
const isCustom = type === 'custom';
const isTextarea = type === 'textarea';
if (!isCustom && InputComponent) {
throw new Error('inputComponent should only be given with type="custom" prop');
}
if (isCustom && !InputComponent) {
throw new Error('inputComponent prop required for custom inputs');
}
// Normal <input> component takes all the props, but the type prop
// is omitted from <textarea> and custom components.
const inputProps = { ...input, type, placeholder, autoFocus };
const { pristine, valid, invalid, touched, error } = meta;
const inputPropsWithoutType = omit(inputProps, 'type');
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;
// Input is market as succesful if it has been changed and
// validation has not failed.
const isFilledInAndValid = !pristine && touched && valid;
const classes = classNames(rootClassName || css.root, className);
const labelClasses = labelRootClassName || css.label;
const inputClasses = classNames(inputRootClassName || css.input, {
[css.inputSuccess]: isFilledInAndValid,
[css.inputSuccess]: valid,
[css.inputError]: hasError,
});
const errorClasses = errorRootClassName || css.validationError;
let component;
if (isCustom) {
component = <InputComponent className={inputRootClassName} {...inputPropsWithoutType} />;
} else if (isTextarea) {
component = <textarea className={inputClasses} {...inputPropsWithoutType} />;
} else {
component = <input className={inputClasses} {...inputProps} />;
}
return (
<div className={classes}>
{label ? <label className={labelClasses} htmlFor={inputProps.name}>{label}</label> : null}
<input className={inputClasses} {...inputProps} />
{label ? <label className={labelClasses} htmlFor={input.name}>{label}</label> : null}
{component}
{hasError ? <p className={errorClasses}>{error}</p> : null}
</div>
);
@ -59,12 +82,14 @@ InputField.defaultProps = {
inputRootClassName: null,
errorRootClassName: null,
clearOnUnmount: false,
inputComponent: null,
type: null,
label: null,
placeholder: null,
autoFocus: false,
};
const { string, shape, bool, func } = PropTypes;
const { string, shape, bool, func, oneOfType } = PropTypes;
InputField.propTypes = {
// Allow passing in classes to subcomponents
@ -76,8 +101,13 @@ InputField.propTypes = {
clearOnUnmount: bool,
// If the type props is 'custom', this prop is used as the component
inputComponent: oneOfType([func, string]),
// 'custom', 'textarea', or something passed to an <input> element
type: string,
// Extra props passed to the underlying input component
type: string.isRequired,
label: string,
placeholder: string,
autoFocus: bool,

View file

@ -10,18 +10,6 @@
flex-grow: 1;
}
.descriptionField {
/* Font */
font-family: inherit;
/* Border */
border-radius: 0px; /* Remove default iOS border radius */
/* Dimensions */
padding: 12px 18px;
min-height: 120px;
}
.submitButton {
margin-top: 1rem;
margin-bottom: 1rem;

View file

@ -1,9 +1,8 @@
import React, { Component, PropTypes } from 'react';
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { enhancedField } from '../../util/forms';
import { maxLength, required } from '../../util/validators';
import { Button, InputField } from '../../components';
@ -11,83 +10,73 @@ import css from './EditListingDescriptionForm.css';
const TITLE_MAX_LENGTH = 60;
export class EditListingDescriptionFormComponent extends Component {
constructor(props) {
super(props);
const EditListingDescriptionFormComponent = props => {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
saveActionMsg,
submitting,
} = props;
// We must create the enhanced components outside the render function
// to avoid losing focus.
// See: https://github.com/erikras/redux-form/releases/tag/v6.0.0-alpha.14
this.EnhancedTextArea = enhancedField('textarea', { rootClassName: css.description });
}
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
const titlePlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titlePlaceholder',
});
const titleRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titleRequired',
});
const maxLengthMessage = intl.formatMessage(
{ id: 'EditListingDescriptionForm.maxLength' },
{
maxLength: TITLE_MAX_LENGTH,
}
);
render() {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
saveActionMsg,
submitting,
} = this.props;
const descriptionMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.description' });
const descriptionPlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionPlaceholder',
});
const maxLength60Message = maxLength(maxLengthMessage, TITLE_MAX_LENGTH);
const descriptionRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionRequired',
});
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
const titlePlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titlePlaceholder',
});
const titleRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titleRequired',
});
const maxLengthMessage = intl.formatMessage(
{ id: 'EditListingDescriptionForm.maxLength' },
{
maxLength: TITLE_MAX_LENGTH,
}
);
const classes = classNames(css.root, className);
return (
<form className={classes} onSubmit={handleSubmit}>
<Field
autoFocus
name="title"
label={titleMessage}
placeholder={titlePlaceholderMessage}
component={InputField}
type="text"
validate={[required(titleRequiredMessage), maxLength60Message]}
/>
const descriptionMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.description' });
const descriptionPlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionPlaceholder',
});
const maxLength60Message = maxLength(maxLengthMessage, TITLE_MAX_LENGTH);
const descriptionRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionRequired',
});
<Field
name="description"
label={descriptionMessage}
placeholder={descriptionPlaceholderMessage}
className={css.description}
type="textarea"
component={InputField}
validate={[required(descriptionRequiredMessage)]}
/>
const classes = classNames(css.root, className);
return (
<form className={classes} onSubmit={handleSubmit}>
<Field
autoFocus
name="title"
label={titleMessage}
placeholder={titlePlaceholderMessage}
component={InputField}
type="text"
validate={[required(titleRequiredMessage), maxLength60Message]}
/>
<Field
name="description"
label={descriptionMessage}
placeholder={descriptionPlaceholderMessage}
className={css.descriptionField}
component={this.EnhancedTextArea}
validate={[required(descriptionRequiredMessage)]}
/>
<Button
className={css.submitButton}
type="submit"
disabled={invalid || submitting || disabled}
>
{saveActionMsg}
</Button>
</form>
);
}
}
<Button
className={css.submitButton}
type="submit"
disabled={invalid || submitting || disabled}
>
{saveActionMsg}
</Button>
</form>
);
};
EditListingDescriptionFormComponent.defaultProps = {
className: null,

View file

@ -25,12 +25,13 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = `
<div
className="">
<label
className=""
className={undefined}
htmlFor="description">
Describe your sauna
</label>
<textarea
className={undefined}
autoFocus={false}
className=""
name="description"
onBlur={[Function]}
onChange={[Function]}

View file

@ -47,7 +47,6 @@ class PayoutDetailsFormComponent extends Component {
constructor(props) {
super(props);
this.EnhancedCountriesDropdown = enhancedField(CountriesSelect);
this.EnhancedBirthdayInput = enhancedField(BirthdayInput);
}
render() {
const {
@ -201,10 +200,11 @@ class PayoutDetailsFormComponent extends Component {
/>
<Field
name="birthDate"
type="text"
label={birthdayLabel}
format={null}
component={this.EnhancedBirthdayInput}
type="custom"
inputComponent={BirthdayInput}
component={InputField}
validate={birthdayRequired}
/>
<h2 className={css.subTitle}>

View file

@ -37,6 +37,9 @@
/* Border radius */
--borderRadius: 2px;
/* Transitions */
--transitionStyle: ease-in 0.2s;
}
/* ================ Custom media queries ================ */
@ -265,9 +268,19 @@ select {
textarea {
width: 100%;
border-width: 1px;
border-style: solid;
border-color: var(--marketplaceColor);
min-height: 48px;
padding: 0 0 5px 0;
/* Borders */
border: none;
border-bottom-width: 3px;
border-bottom-style: solid;
border-bottom-color: var(--marketplaceColor);
border-radius: 0;
&:focus {
outline: none;
}
}
input {
@ -280,6 +293,8 @@ input {
border-bottom-color: var(--marketplaceColor);
border-radius: 0;
transition: border-bottom-color var(--transitionStyle);
&::placeholder {
color: var(--matterColorAnti);
}