appbaseio/reactivesearch

View on GitHub
packages/web/src/components/date/DateRange.js

Summary

Maintainability
F
5 days
Test Coverage
import React, { Component } from 'react';
import {
    addComponent,
    removeComponent,
    watchComponent,
    updateQuery,
    setQueryListener,
} from '@appbaseio/reactivecore/lib/actions';
import {
    isEqual,
    checkValueChange,
    checkPropChange,
    checkSomePropChange,
    getClassName,
    formatDate,
} from '@appbaseio/reactivecore/lib/utils/helper';
import types from '@appbaseio/reactivecore/lib/utils/types';
import XDate from 'xdate';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import { withTheme } from 'emotion-theming';

import DateContainer from '../../styles/DateContainer';
import Title from '../../styles/Title';
import Flex from '../../styles/Flex';
import { connect } from '../../utils';

import CancelSvg from '../shared/CancelSvg';

class DateRange extends Component {
    constructor(props) {
        super(props);
        this.state = {
            currentDate: null,
            dateHovered: null,
        };
        this.locked = false;
        props.setQueryListener(props.componentId, props.onQueryChange, null);
    }

    componentWillMount() {
        this.props.addComponent(this.props.componentId);
        this.setReact(this.props);

        if (this.props.selectedValue) {
            // parsing string values from selectedValue to date objects for DayPicker
            // for value as an array from URL
            if (Array.isArray(this.props.selectedValue)) {
                this.handleDateChange(
                    {
                        start: new Date(this.props.selectedValue[0]),
                        end: new Date(this.props.selectedValue[1]),
                    },
                    false,
                );
            } else {
                // for value as an object for SSR
                const { start, end } = this.props.selectedValue;
                this.handleDateChange(
                    {
                        start: new Date(start),
                        end: new Date(end),
                    },
                    false,
                );
            }
        } else if (this.props.defaultSelected) {
            this.handleDateChange(this.props.defaultSelected, false);
        }
    }

    componentWillReceiveProps(nextProps) {
        checkPropChange(this.props.react, nextProps.react, () => this.setReact(nextProps));
        if (!isEqual(this.props.defaultSelected, nextProps.defaultSelected)) {
            this.handleDateChange(nextProps.defaultSelected, false, nextProps);
        } else {
            const { currentDate } = this.state;
            const { selectedValue } = nextProps;
            // comparing array format of selectedValue with object form of the state if not null
            if (
                !isEqual(
                    currentDate
                        ? [
                            this.formatInputDate(currentDate.start),
                            this.formatInputDate(currentDate.end),
                        ] // prettier-ignore
                        : null,
                    selectedValue,
                )
                && !isEqual(this.props.selectedValue, selectedValue)
            ) {
                this.handleDateChange(
                    selectedValue
                        ? {
                            start: nextProps.selectedValue[0],
                            end: nextProps.selectedValue[1],
                        } // prettier-ignore
                        : null,
                    true,
                    nextProps,
                );
            }
        }
        checkSomePropChange(this.props, nextProps, ['dataField', 'nestedField'], () =>
            this.updateQuery(
                this.state.currentDate
                    ? {
                        // we need the date in correct queryFormat
                        start: formatDate(this.state.currentDate.start),
                        end: formatDate(this.state.currentDate.end),
                    } // prettier-ignore
                    : this.state.currentDate,
                nextProps,
            ),
        );
    }

    componentWillUnmount() {
        this.props.removeComponent(this.props.componentId);
    }

    setReact(props) {
        if (props.react) {
            props.watchComponent(props.componentId, props.react);
        }
    }

    formatInputDate = (date) => {
        const xdate = new XDate(date);
        return xdate.valid() ? xdate.toString('yyyy-MM-dd') : '';
    };

    static defaultQuery = (value, props) => {
        let query = null;
        if (value) {
            if (Array.isArray(props.dataField) && props.dataField.length === 2) {
                query = {
                    bool: {
                        must: [
                            {
                                range: {
                                    [props.dataField[0]]: {
                                        lte: formatDate(new XDate(value.start), props),
                                    },
                                },
                            },
                            {
                                range: {
                                    [props.dataField[1]]: {
                                        gte: formatDate(new XDate(value.end), props),
                                    },
                                },
                            },
                        ],
                    },
                };
            } else if (Array.isArray(props.dataField)) {
                query = {
                    range: {
                        [props.dataField[0]]: {
                            gte: formatDate(new XDate(value.start), props),
                            lte: formatDate(new XDate(value.end), props),
                        },
                    },
                };
            } else {
                query = {
                    range: {
                        [props.dataField]: {
                            gte: formatDate(new XDate(value.start), props),
                            lte: formatDate(new XDate(value.end), props),
                        },
                    },
                };
            }
        }

        if (query && props.nestedField) {
            return {
                query: {
                    nested: {
                        path: props.nestedField,
                        query,
                    },
                },
            };
        }

        return query;
    };

    getEndDateRef = (ref) => {
        this.endDateRef = ref;
    };

    getStartDateRef = (ref) => {
        this.startDateRef = ref;
    };

    clearDayPickerStart = () => {
        if (this.state.currentDate && this.state.currentDate.start !== '') {
            this.handleStartDate('', false); // resets the day picker component
        }
    };

    clearDayPickerEnd = () => {
        if (this.state.currentDate && this.state.currentDate.end !== '') {
            this.handleEndDate(''); // resets the day picker component
        }
    };

    handleStartDate = (date, autoFocus = true) => {
        const { currentDate } = this.state;
        const end = currentDate ? currentDate.end : '';
        if (this.startDateRef.getInput().value.length === 10) {
            this.handleDateChange({
                start: date,
                end,
            });
            // focus the end date DayPicker if its empty
            if (this.props.autoFocusEnd && autoFocus) {
                this.endDateRef.getInput().focus();
            }
        }
    };

    handleEndDate = (selectedDay) => {
        const { currentDate } = this.state;
        if (this.endDateRef.getInput().value.length === 10) {
            this.handleDayMouseEnter(selectedDay);
            this.handleDateChange({
                start: currentDate ? currentDate.start : '',
                end: selectedDay,
            });
        }
    };

    handleDayMouseEnter = (day) => {
        this.setState({
            dateHovered: day,
        });
    };

    handleDateChange = (currentDate, isDefaultValue = false, props = this.props) => {
        // ignore state updates when component is locked
        if (props.beforeValueChange && this.locked) {
            return;
        }

        this.locked = true;
        let value = null;
        if (currentDate && !(currentDate.start === '' && currentDate.end === '')) {
            value = isDefaultValue
                ? currentDate
                : {
                    start: this.formatInputDate(currentDate.start),
                    end: this.formatInputDate(currentDate.end),
                }; // prettier-ignore
        }

        const performUpdate = () => {
            this.setState(
                {
                    currentDate,
                },
                () => {
                    this.updateQuery(value, props);
                    this.locked = false;
                    if (props.onValueChange) props.onValueChange(value);
                },
            );
        };
        checkValueChange(props.componentId, value, props.beforeValueChange, performUpdate);
    };

    updateQuery = (value, props) => {
        if (!value || (value && value.start.length && value.end.length)) {
            const query = props.customQuery || DateRange.defaultQuery;

            props.updateQuery({
                componentId: props.componentId,
                query: query(value, props),
                value: value ? [value.start, value.end] : null,
                showFilter: props.showFilter,
                label: props.filterLabel,
                URLParams: props.URLParams,
                componentType: 'DATERANGE',
            });
        }
    };

    render() {
        const { currentDate, dateHovered } = this.state;
        const start = currentDate ? currentDate.start : '';
        const end = currentDate ? currentDate.end : '';
        const endDay = dateHovered || '';
        const selectedDays = [start, { from: start, to: endDay }];
        const modifiers = { start, end: endDay };
        return (
            <DateContainer
                range
                style={this.props.style}
                className={this.props.className}
                showBorder={!this.props.showClear}
            >
                {this.props.title && (
                    <Title className={getClassName(this.props.innerClass, 'title') || null}>
                        {this.props.title}
                    </Title>
                )}
                <Flex className={getClassName(this.props.innerClass, 'input-container') || null}>
                    <Flex
                        flex={2}
                        showBorder={this.props.showClear}
                        iconPosition="right"
                        style={{
                            background: this.props.theme.colors.backgroundColor || 'transparent',
                        }}
                    >
                        <DayPickerInput
                            ref={this.getStartDateRef}
                            showOverlay={this.props.focused}
                            formatDate={this.formatInputDate}
                            value={start}
                            placeholder={this.props.placeholder.start}
                            dayPickerProps={{
                                numberOfMonths: this.props.numberOfMonths,
                                initialMonth: this.props.initialMonth,
                                disabledDays: {
                                    after: this.state.currentDate ? this.state.currentDate.end : '',
                                },
                                selectedDays,
                                modifiers,
                            }}
                            onDayChange={this.handleStartDate}
                            classNames={{
                                container:
                                    getClassName(this.props.innerClass, 'daypicker-container')
                                    || 'DayPickerInput',
                                overlayWrapper:
                                    getClassName(
                                        this.props.innerClass,
                                        'daypicker-overlay-wrapper',
                                    ) || 'DayPickerInput-OverlayWrapper',
                                overlay:
                                    getClassName(this.props.innerClass, 'daypicker-overlay')
                                    || 'DayPickerInput-Overlay',
                            }}
                            {...this.props.dayPickerInputProps}
                        />
                        {this.props.showClear
                            && this.state.currentDate
                            && this.state.currentDate.start && (
                            <CancelSvg onClick={this.clearDayPickerStart} />
                        )}
                    </Flex>
                    <Flex justifyContent="center" alignItems="center" basis="20px">
                        -
                    </Flex>
                    <Flex
                        flex={2}
                        showBorder={this.props.showClear}
                        iconPosition="right"
                        style={{
                            background: this.props.theme.colors.backgroundColor || 'transparent',
                        }}
                    >
                        <DayPickerInput
                            ref={this.getEndDateRef}
                            showOverlay={this.props.focused}
                            formatDate={this.formatInputDate}
                            value={end}
                            placeholder={this.props.placeholder.end}
                            dayPickerProps={{
                                numberOfMonths: this.props.numberOfMonths,
                                initialMonth: this.props.initialMonth,
                                onDayMouseEnter: this.handleDayMouseEnter,
                                disabledDays: {
                                    before: this.state.currentDate
                                        ? this.state.currentDate.start
                                        : '',
                                },
                                selectedDays,
                                modifiers,
                            }}
                            onDayChange={this.handleEndDate}
                            classNames={{
                                container:
                                    getClassName(this.props.innerClass, 'daypicker-container')
                                    || 'DayPickerInput',
                                overlayWrapper:
                                    getClassName(
                                        this.props.innerClass,
                                        'daypicker-overlay-wrapper',
                                    ) || 'DayPickerInput-OverlayWrapper',
                                overlay:
                                    getClassName(this.props.innerClass, 'daypicker-overlay')
                                    || 'DayPickerInput-Overlay',
                            }}
                            {...this.props.dayPickerInputProps}
                        />
                        {this.props.showClear
                            && this.state.currentDate
                            && this.state.currentDate.end && (
                            <CancelSvg onClick={this.clearDayPickerEnd} />
                        )}
                    </Flex>
                </Flex>
            </DateContainer>
        );
    }
}

DateRange.propTypes = {
    addComponent: types.funcRequired,
    removeComponent: types.funcRequired,
    setQueryListener: types.funcRequired,
    updateQuery: types.funcRequired,
    watchComponent: types.funcRequired,
    selectedValue: types.selectedValue,
    // component props
    autoFocusEnd: types.bool,
    className: types.string,
    componentId: types.stringRequired,
    dataField: types.dataFieldArray,
    dayPickerInputProps: types.props,
    defaultSelected: types.dateObject,
    filterLabel: types.string,
    focused: types.bool,
    initialMonth: types.dateObject,
    innerClass: types.style,
    nestedField: types.string,
    numberOfMonths: types.number,
    onQueryChange: types.func,
    parseDate: types.func,
    placeholder: types.rangeLabels,
    queryFormat: types.queryFormatDate,
    react: types.react,
    showClear: types.bool,
    showFilter: types.bool,
    style: types.style,
    theme: types.style,
    title: types.string,
};

DateRange.defaultProps = {
    autoFocusEnd: true,
    numberOfMonths: 2,
    placeholder: {
        start: 'Start date',
        end: 'End date',
    },
    showClear: true,
    showFilter: true,
};

const mapStateToProps = (state, props) => ({
    selectedValue: state.selectedValues[props.componentId]
        ? state.selectedValues[props.componentId].value
        : null,
});

const mapDispatchtoProps = dispatch => ({
    addComponent: component => dispatch(addComponent(component)),
    removeComponent: component => dispatch(removeComponent(component)),
    updateQuery: updateQueryObject => dispatch(updateQuery(updateQueryObject)),
    watchComponent: (component, react) => dispatch(watchComponent(component, react)),
    setQueryListener: (component, onQueryChange, beforeQueryChange) =>
        dispatch(setQueryListener(component, onQueryChange, beforeQueryChange)),
});

export default connect(
    mapStateToProps,
    mapDispatchtoProps,
)(withTheme(DateRange));