Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

support datepickersingle as an uncontrolled component #532

Merged
merged 16 commits into from
Apr 20, 2019
60 changes: 40 additions & 20 deletions src/components/DatePickerRange.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ export default class DatePickerRange extends Component {
propsToState(newProps) {
/*
* state includes:
* - user modifiable attributes
* - if no ID, user modifiable attributes (date)
* - moment converted attributes
*/

const newState = convertToMoment(newProps, [
'start_date',
'end_date',
'initial_visible_month',
'max_date_allowed',
'min_date_allowed',
]);

if (!newProps.id) {
newState.start_date = newProps.start_date;
newState.end_date = newProps.end_date;
}
this.setState(newState);
}

Expand All @@ -49,30 +51,39 @@ export default class DatePickerRange extends Component {
componentWillMount() {
this.propsToState(this.props);
}

onDatesChange({startDate: start_date, endDate: end_date}) {
const {setProps, updatemode} = this.props;
let payload;

const old_start_date = this.state.start_date;
const old_end_date = this.state.end_date;
const oldMomentDates = this.props.id
? convertToMoment(this.props, ['start_date', 'end_date'])
: convertToMoment(this.state, ['start_date', 'end_date']);

this.setState({start_date, end_date});

if (start_date && !start_date.isSame(old_start_date)) {
if (start_date && !start_date.isSame(oldMomentDates.start_date)) {
if (updatemode === 'singledate') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this swallow the start date in bothdates
mode

setProps({start_date: start_date.format('YYYY-MM-DD')});
payload = {start_date: start_date.format('YYYY-MM-DD')};
} else {
this.setState({start_date: start_date.format('YYYY-MM-DD')})
}
}

if (end_date && !end_date.isSame(old_end_date)) {
if (end_date && !end_date.isSame(oldMomentDates.end_date)) {
if (updatemode === 'singledate') {
setProps({end_date: end_date.format('YYYY-MM-DD')});
payload = {end_date: end_date.format('YYYY-MM-DD')};
} else if (updatemode === 'bothdates') {
setProps({
start_date: start_date.format('YYYY-MM-DD'),
payload = {
start_date: this.state.start_date,
end_date: end_date.format('YYYY-MM-DD'),
});
};
}
}

if (this.props.id) {
setProps(payload);
} else {
this.setState(payload);
}
}

isOutsideRange(date) {
Expand All @@ -85,12 +96,7 @@ export default class DatePickerRange extends Component {
}

render() {
const {
start_date,
end_date,
focusedInput,
initial_visible_month,
} = this.state;
const {focusedInput, initial_visible_month} = this.state;

const {
calendar_orientation,
Expand Down Expand Up @@ -118,6 +124,20 @@ export default class DatePickerRange extends Component {
end_date_id,
} = this.props;

let start_date;
let end_date;
if (id) {
({start_date, end_date} = convertToMoment(this.props, [
'start_date',
'end_date',
]));
} else {
({start_date, end_date} = convertToMoment(this.state, [
'start_date',
'end_date',
]));
}

const verticalFlag = calendar_orientation !== 'vertical';

const DatePickerWrapperStyles = {
Expand Down
47 changes: 40 additions & 7 deletions src/components/DatePickerSingle.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@ export default class DatePickerSingle extends Component {
propsToState(newProps) {
/*
* state includes:
* - user modifiable attributes
* - if no ID, user modifiable attributes (date)
* - moment converted attributes
*/

const newState = convertToMoment(newProps, [
'date',
'initial_visible_month',
'max_date_allowed',
'min_date_allowed',
]);

if (!newProps.id) {
newState.date = newProps.date;
}
this.setState(newState);
}

Expand All @@ -62,14 +64,38 @@ export default class DatePickerSingle extends Component {
}

onDateChange(date) {
const {setProps} = this.props;

this.setState({date});
setProps({date: date ? date.format('YYYY-MM-DD') : null});
const {id, setProps} = this.props;
const payload = {date: date ? date.format('YYYY-MM-DD') : null};

if (!id) {
/*
* dash-renderer will control this component
* if the component has an ID.
* If it doesn't, then this component needs to
* manage its own state.
*
* In the future, dash-renderer may be able to
* handle the state no matter what:
* https://github.com/plotly/dash-renderer/issues/163
*
* In almost all practical cases, these controls
* will have an ID (as they are inputs to callbacks)
* but as users are authoring their app's layout,
* they may include some controls without IDs
* to start. If we don't manage the state, then
* the user may be surprised the component reacts
* different to user input when it is "unconnected"
* (without an ID) vs when it is connected.
*
*/
this.setState(payload);
} else {
setProps(payload);
}
}

render() {
const {date, focused, initial_visible_month} = this.state;
const {focused, initial_visible_month} = this.state;

const {
calendar_orientation,
Expand All @@ -93,6 +119,13 @@ export default class DatePickerSingle extends Component {
className,
} = this.props;

let date;
if (id) {
date = convertToMoment(this.props, ['date']).date;
} else {
date = convertToMoment(this.state, ['date']).date;
}

const verticalFlag = calendar_orientation !== 'vertical';

const DatePickerWrapperStyles = {
Expand Down
34 changes: 22 additions & 12 deletions test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
# export PERCY_PROJECT=plotly/dash-integration-tests
# export PERCY_TOKEN=...

TIMEOUT = 20

TIMEOUT = 10
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😸



class Tests(IntegrationTests):
Expand Down Expand Up @@ -1658,19 +1659,28 @@ def update_output(start_date, end_date):

self.wait_for_text_to_equal('#date-picker-range-output', 'None - None')

# updated only one date, callback shouldn't fire and output should be unchanged
dt_length = len(start_date.get_attribute('value'))
start_date.send_keys(dt_length * Keys.BACKSPACE)
start_date.send_keys("1997-05-03")
# using mouse click with fixed day range, this can be improved
# once we start refactoring the test structure
start_date.click()

sday = self.driver.find_element_by_xpath("//td[text()='1' and @tabindex='0']")
sday.click()
self.wait_for_text_to_equal('#date-picker-range-output', 'None - None')

# updated both dates, callback should now fire and update output
dt_length = len(end_date.get_attribute('value'))
end_date.send_keys(dt_length * Keys.BACKSPACE)
end_date.send_keys("1997-05-04")
end_date.click()
self.wait_for_text_to_equal(
'#date-picker-range-output', '1997-05-03 - 1997-05-04')
eday = self.driver.find_elements_by_xpath("//td[text()='4']")[1]
eday.click()

date_tokens = set(start_date.get_attribute('value').split('/'))
date_tokens.update(end_date.get_attribute('value').split('/'))

self.assertEqual(
set(itertools.chain(*[
_.split('-')
for _ in self.driver.find_element_by_css_selector(
'#date-picker-range-output').text.split(' - ')])
),
date_tokens,
"date should match the callback output")

def test_interval(self):
app = dash.Dash(__name__)
Expand Down
5 changes: 1 addition & 4 deletions test/unit/DatePickerRange.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {mount, render} from 'enzyme';
const defaultProps = {
start_date_id: 'start-date-id',
end_date_id: 'end-date-id',
id: 'datepicker',
};

test('DatePickerRange renders', () => {
Expand All @@ -25,9 +26,7 @@ describe('Date can be set properly', () => {

expect(dps.props()).toBeDefined();
expect(dps.props().end_date).toEqual(props.end_date);
expect(dps.state().end_date).toEqual(null);
expect(dps.props().start_date).toEqual(props.start_date);
expect(dps.state().start_date).toEqual(null);
});

test('valid date is not converted by moment', () => {
Expand All @@ -40,8 +39,6 @@ describe('Date can be set properly', () => {

expect(dps.props()).toBeDefined();
expect(dps.props().end_date).toEqual(props.end_date);
expect(dps.state().end_date).not.toEqual(null);
expect(dps.props().start_date).toEqual(props.start_date);
expect(dps.state().start_date).not.toEqual(null);
});
});
8 changes: 5 additions & 3 deletions test/unit/DatePickerSingle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ test('DatePickerSingle renders', () => {
});

describe('Date can be set properly', () => {
const defaultProps = {};
const defaultProps = {
id: 'datepicker',
};

test('null date is not converted by moment', () => {
const props = R.merge(defaultProps, {
Expand All @@ -21,7 +23,6 @@ describe('Date can be set properly', () => {

expect(dps.props()).toBeDefined();
expect(dps.props().date).toEqual(props.date);
expect(dps.state().date).toEqual(null);
});

test('valid date is not converted by moment', () => {
Expand All @@ -33,14 +34,14 @@ describe('Date can be set properly', () => {

expect(dps.props()).toBeDefined();
expect(dps.props().date).toEqual(props.date);
expect(dps.state().date).not.toEqual(null);
});
});

describe('Date can be selected', () => {
test('`setProps` callback is called when date is selected', () => {
const setPropsSpy = jest.fn();
const props = {
id: 'datepicker',
date: '2019-01-01',
placeholder: 'My Date',
setProps: setPropsSpy,
Expand All @@ -59,6 +60,7 @@ describe('Date can be cleared', () => {
test('`setProps` callback is called when date is cleared', () => {
const setPropsSpy = jest.fn();
const props = {
id: 'datepicker',
date: '2019-01-01',
clearable: true,
setProps: setPropsSpy,
Expand Down
2 changes: 1 addition & 1 deletion test/unit/Input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Props can be set properly', () => {
className: 'input-class',
type: 'text',
autoComplete: 'on',
autoFocus: 'on',
autoFocus: 'autofocus',
disabled: true,
debounce: false,
inputMode: 'verbatim',
Expand Down