BirtdayInput component

This commit is contained in:
Kimmo Puputti 2017-05-09 17:07:55 +03:00
parent 389e376abf
commit 830f06922f
5 changed files with 255 additions and 0 deletions

View file

@ -0,0 +1,32 @@
.root {
display: flex;
}
.dropdown {
display: inline-block;
flex: 1;
appearance: none;
/* Font */
font-size: 16px;
letter-spacing: -0.4px;
/* Border */
border: 1px solid #979797;
border-left-width: 0;
border-radius: 0;
&:first-child {
border-left-width: 1px;
}
/* Dimensions */
padding-left: 18px;
padding-right: 18px;
height: 50px;
/* Background */
background-image: url('data:image/svg+xml;utf8,<svg width="16" height="10" viewBox="0 0 16 10" xmlns="http://www.w3.org/2000/svg"><path d="M15.027 2.5a.577.577 0 0 0 0-.813L13.545.214a.566.566 0 0 0-.804 0L8 4.955 3.259.215a.566.566 0 0 0-.804 0L.973 1.686a.577.577 0 0 0 0 .813l6.625 6.616c.223.223.58.223.804 0L15.027 2.5z" fill="#4A4A4A" fill-rule="evenodd"/></svg>');
background-size: 16px 16px;
background-position: center right 10px;
}

View file

@ -0,0 +1,40 @@
/* eslint-disable no-console */
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import * as validators from '../../util/validators';
import { enhancedField } from '../../util/forms';
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 Form = reduxForm({
form: 'Styleguide.BirthdayInput.Form',
})(FormComponent);
export const Empty = {
component: Form,
props: {
onChange: values => {
console.log('birthday changed to:', values.birthday);
},
},
};

View file

@ -0,0 +1,179 @@
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import { range } from 'lodash';
import css from './BirthdayInput.css';
// Since redux-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.
//
// This prevents showing the validation error when the user selects a
// value and moves on to another input within this component.
const BLUR_TIMEOUT = 100;
const pad = num => {
if (num >= 0 && num < 10) {
return `0${num}`;
}
return num.toString();
};
const parseNum = str => {
const num = parseInt(str, 10);
return isNaN(num) ? null : num;
};
// Validate that the given date has the same info as the selected
// value, i.e. it has not e.g. rolled over to the next month if the
// selected month doesn't have as many days as selected.
const isValidDate = (date, year, month, day) => {
const yearsMatch = date.getFullYear() === year;
const monthsMatch = date.getMonth() + 1 === month;
const daysMatch = date.getDate() === day;
return yearsMatch && monthsMatch && daysMatch;
};
// Create a Date from the selected values. Return null if the date is
// invalid.
const dateFromSelected = ({ day, month, year }) => {
const dayNum = parseNum(day);
const monthNum = parseNum(month);
const yearNum = parseNum(year);
if (dayNum !== null && monthNum !== null && yearNum !== null) {
const d = new Date(Date.UTC(yearNum, monthNum - 1, dayNum));
return isValidDate(d, yearNum, monthNum, dayNum) ? d : null;
}
return null;
};
const selectedFromDate = date => ({
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear(),
});
// Always show 31 days per month
const days = range(1, 32);
const months = range(1, 13);
// Show a certain number of years up to the current year
const currentYear = new Date().getFullYear();
const yearsToShow = 80;
const years = range(currentYear, currentYear - yearsToShow, -1);
class BirthdayInput extends Component {
constructor(props) {
super(props);
this.state = {
selected: {
day: null,
month: null,
year: null,
},
};
this.blurTimeoutId = null;
this.handleSelectFocus = this.handleSelectFocus.bind(this);
this.handleSelectBlur = this.handleSelectBlur.bind(this);
this.handleSelectChange = this.handleSelectChange.bind(this);
}
componentWillMount() {
const value = this.props.input.value;
if (value instanceof Date) {
this.setState({ selected: selectedFromDate(value) });
}
}
componentWillReceiveProps(newProps) {
const oldValue = this.props.input.value;
const newValue = newProps.input.value;
const valueChanged = oldValue !== newValue;
if (valueChanged && newValue instanceof Date) {
this.setState({ selected: selectedFromDate(newValue) });
}
}
componentWillUnmount() {
window.clearTimeout(this.blurTimeoutId);
}
handleSelectFocus() {
window.clearTimeout(this.blurTimeoutId);
this.props.input.onFocus();
}
handleSelectBlur() {
window.clearTimeout(this.blurTimeoutId);
this.blurTimeoutId = window.setTimeout(
() => {
this.props.input.onBlur();
},
BLUR_TIMEOUT
);
}
handleSelectChange(type, value) {
this.setState(prevState => {
const selected = { ...prevState.selected, [type]: parseNum(value) };
this.props.input.onChange(dateFromSelected(selected));
return { selected };
});
}
render() {
const { className } = this.props;
const selectedValue = n => {
return typeof n === 'number' ? n : '';
};
const classes = classNames(css.root, className);
return (
<div className={classes}>
<select
value={selectedValue(this.state.selected.day)}
className={css.dropdown}
onFocus={() => this.handleSelectFocus()}
onBlur={() => this.handleSelectBlur()}
onChange={e => this.handleSelectChange('day', e.target.value)}
>
<option />
{days.map(d => <option key={d} value={d}>{pad(d)}</option>)}
</select>
<select
value={selectedValue(this.state.selected.month)}
className={css.dropdown}
onFocus={() => this.handleSelectFocus()}
onBlur={() => this.handleSelectBlur()}
onChange={e => this.handleSelectChange('month', e.target.value)}
>
<option />
{months.map(m => <option key={m} value={m}>{pad(m)}</option>)}
</select>
<select
value={selectedValue(this.state.selected.year)}
className={css.dropdown}
onFocus={() => this.handleSelectFocus()}
onBlur={() => this.handleSelectBlur()}
onChange={e => this.handleSelectChange('year', e.target.value)}
>
<option />
{years.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
);
}
}
BirthdayInput.defaultProps = { className: '' };
const { string, shape, instanceOf, func } = PropTypes;
BirthdayInput.propTypes = {
className: string,
input: shape({
name: string.isRequired,
value: instanceOf(Date),
onChange: func.isRequired,
onFocus: func.isRequired,
onBlur: func.isRequired,
}).isRequired,
};
export default BirthdayInput;

View file

@ -1,6 +1,7 @@
import AddImages from './AddImages/AddImages';
import AuthorInfo from './AuthorInfo/AuthorInfo';
import Avatar from './Avatar/Avatar';
import BirthdayInput from './BirthdayInput/BirthdayInput';
import BookingInfo from './BookingInfo/BookingInfo';
import Button, { FlatButton, InlineButton } from './Button/Button';
import CurrencyInput from './CurrencyInput/CurrencyInput';
@ -39,6 +40,7 @@ export {
AddImages,
AuthorInfo,
Avatar,
BirthdayInput,
BookingInfo,
Button,
CurrencyInput,

View file

@ -1,5 +1,6 @@
// components
import * as AddImages from './components/AddImages/AddImages.example';
import * as BirthdayInput from './components/BirthdayInput/BirthdayInput.example';
import * as BookingInfo from './components/BookingInfo/BookingInfo.example';
import * as CurrencyInput from './components/CurrencyInput/CurrencyInput.example';
import * as DateInput from './components/DateInput/DateInput.example';
@ -30,6 +31,7 @@ import * as StripePaymentForm from './containers/StripePaymentForm/StripePayment
export {
AddImages,
BirthdayInput,
BookingDatesForm,
BookingInfo,
ChangeAccountPasswordForm,