mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 20:53:24 +10:00
Merge pull request #210 from sharetribe/use-input-field-in-forms
Use InputField in forms
This commit is contained in:
commit
1dbbc4e66c
10 changed files with 174 additions and 145 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue