mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 09:13:14 +10:00
BirtdayInput component
This commit is contained in:
parent
389e376abf
commit
830f06922f
5 changed files with 255 additions and 0 deletions
32
src/components/BirthdayInput/BirthdayInput.css
Normal file
32
src/components/BirthdayInput/BirthdayInput.css
Normal 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;
|
||||
}
|
||||
40
src/components/BirthdayInput/BirthdayInput.example.js
Normal file
40
src/components/BirthdayInput/BirthdayInput.example.js
Normal 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);
|
||||
},
|
||||
},
|
||||
};
|
||||
179
src/components/BirthdayInput/BirthdayInput.js
Normal file
179
src/components/BirthdayInput/BirthdayInput.js
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue