Skip to content

Instantly share code, notes, and snippets.

@AnatoliyLitinskiy
Created July 5, 2018 07:49
Show Gist options
  • Save AnatoliyLitinskiy/f8285be07e34c4be255145624572d9f5 to your computer and use it in GitHub Desktop.
Save AnatoliyLitinskiy/f8285be07e34c4be255145624572d9f5 to your computer and use it in GitHub Desktop.
react input format + avoid caret jumping
import React from 'react';
import PropTypes from 'prop-types';
import { parseDate, formatInput, formatDate } from './helpers';
class FormattedDateField extends React.PureComponent {
static propTypes = {
date: PropTypes.instanceOf(Date),
onChange: PropTypes.func.isRequired,
};
state = {
raw: '',
};
get value() {
const { date } = this.props;
const { raw } = this.state;
if (raw) return raw;
if (!date) return '';
return formatDate(date);
}
onChange = (ev) => {
const raw = ev.target.value;
console.log('%c onChange', 'color: gray', { raw });
if (!raw) {
this.setState({ raw: '' });
this.props.onChange(null);
return;
}
const { selectionEnd } = this.inputElement;
/**
* avoid caret position jumping to the end
**/
if (selectionEnd !== raw.length) {
this.setState({ raw });
return;
}
try {
const { date } = parseDate(raw, true);
this.setState({ raw: '' });
this.props.onChange(date);
} catch (err) {
console.log('onChange err', 'color: red', { err });
/**
* format invalid date
**/
const formattedRaw = formatInput(raw);
this.setState({
raw: formattedRaw,
});
}
}
onBlur = () => {
const raw = this.value;
if (!raw) {
this.setState({ raw: '' });
this.props.onChange(null);
}
try {
const { date } = parseDate(raw, false);
this.setState({ raw: '' });
this.props.onChange(date);
} catch (err) {
// do nothing
}
}
render() {
const { date: d } = this.props;
return (
<div>
<pre style={{display: 'block', backgroundColor: 'rgba(0, 0, 0, 0.2)', padding: '8px', border: '1px solid #AAA'}}>
{'{\n'}
value: "{this.value}" ({d ? `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}` : 'no date'}),{'\n'}
raw: "{this.state.raw}",{'\n'}
{'}'}
</pre>
<input
ref={inputElement => this.inputElement = inputElement}
onChange={this.onChange}
value={this.value}
placeholder="MM/DD/YYYY"
onBlur={this.onBlur}
/>
</div>
);
}
}
export default FormattedDateField;
export const parseDate = (str, strong) => {
const parts = ['month', 'day', 'year'];
const assert = (message, f) => (v, i) => { if (!f(v, i)) throw new Error(`assert ${message} ${parts[i]}`); }
const checkLength = (v) => {
if (strong) {
[2, 2, 4].forEach(assert('length equal', (length, index) => v[index].length === length));
} else {
[2, 2, 4].forEach((length, index) => assert(v[index].length <= length));
[2, 2, 1].forEach((length, index) => assert(v[index].length >= length));
}
return v;
}
const ret = [
v => v.replace(/[^\d]/g, ''),
v => console.log('%c onChange replace date', 'color: violet', { v }) || v,
v => [v.substr(0, 2), v.substr(2, 2), v.substr(4, 4)],
checkLength,
v => [12, 31, 9999].forEach(assert('max', (max, index) => v[index] <= max)) || v,
v => [1, 1, -9999].forEach(assert('min', (min, index) => v[index] >= min)) || v,
v => {
const date = new Date(v[2], v[0] - 1, v[1]);
date.setFullYear(v[2]);
return {
date,
day: v[1],
month: v[0],
year: v[2],
}
},
v => console.log('%c onChange end date', 'color: violet', { ...v }) || v,
].reduce((prev, f) => f(prev), str);
return ret;
}
export const formatInput = (valueIn) =>
[
v => v.replace(/[^\d]/g, ''),
v => [v.substr(0, 2), v.substr(2, 2), v.substr(4, 4)],
v => v.reduce((prev, item) => prev + ((item && prev) ? `/${item}`: item), ''),
].reduce((prev, f) => f(prev), valueIn);
export const formatDate = (d, separator = '/') => console.log('%c formatDate', 'color: red', d) || [
('00' + (d.getMonth() + 1)).substr(-2),
('00' + d.getDate()).substr(-2),
('0000' + d.getFullYear()).substr((d.getFullYear() < 99) ? -4 : -d.getFullYear().toString().length),
].join(separator);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment