From 1550377f39e55cbf4673b72b8f645126481b67a5 Mon Sep 17 00:00:00 2001 From: Tessa Thornton Date: Thu, 5 Sep 2019 09:51:51 -0400 Subject: [PATCH] update durationfield component to not use lifecycle methods (#2502) * update durationfield component to not use lifecycle methods * fix: default param cleanup --- .../framework/components/DurationField.tsx | 232 ++++++++---------- 1 file changed, 106 insertions(+), 126 deletions(-) diff --git a/src/core/client/framework/components/DurationField.tsx b/src/core/client/framework/components/DurationField.tsx index e5fd4769c..d13248802 100644 --- a/src/core/client/framework/components/DurationField.tsx +++ b/src/core/client/framework/components/DurationField.tsx @@ -1,5 +1,12 @@ import { Localized } from "fluent-react/compat"; -import React, { ChangeEvent, Component } from "react"; +import React, { + ChangeEvent, + FunctionComponent, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { UNIT } from "coral-common/helpers/i18n"; import { @@ -35,27 +42,16 @@ interface Props { units?: ReadonlyArray; } -interface State { - /** Current value */ - value: string; - /** Current unit */ - unit?: UNIT; - /** All available units */ - units: ReadonlyArray; - /** - * Element callbacks to generate the rendered - * Option element for the select field - */ - elementCallbacks: ReadonlyArray; +function convertToSeconds(value: string, unit?: UNIT) { + const parsed = parseInt(value, 10); + return (isNaN(parsed) || !unit ? value : parsed * unit).toString(); } -/** - * valueToState converts the value we receive from props to a new state. - * @param value The value that was passed through props. - * @param units The units that we use. - * @param unit The current value if any otherwise the best matching unit will be used. - */ -function valueToState(value: string, units: ReadonlyArray, unit?: UNIT) { +function convertFromSeconds( + value: string, + units: ReadonlyArray, + unit?: UNIT +) { const parsed = parseInt(value, 10); // If value was a valid number.. @@ -76,126 +72,110 @@ function valueToState(value: string, units: ReadonlyArray, unit?: UNIT) { return { unit, value, - units, - elementCallbacks: units.map(k => DURATION_UNIT_MAP[k]), }; } -/** - * stateToValue converts current state to the value we pass to onChange. - * @param state - */ -function stateToValue(state: State) { - const parsed = parseInt(state.value, 10); - // If state.value was a number, return computed result, otherwise return the string. - return (isNaN(parsed) ? state.value : parsed * state.unit!).toString(); -} - /** * Duration Field renders a TextField that accepts a number and a SelectField with a unit. * If the entered value is a valid number, it'll propagate the computed value in seconds via * onChange otherwise it'll just propogate whatever was entered as the value TextField. */ -class DurationField extends Component { - public static defaultProps: Partial = { - units: [UNIT.HOURS, UNIT.DAYS, UNIT.WEEKS], - }; +const DurationField: FunctionComponent = ({ + value, + units = [UNIT.HOURS, UNIT.DAYS, UNIT.WEEKS], + onChange, + disabled, + name, +}) => { + const [selectedUnit, setSelectedUnit] = useState( + convertFromSeconds(value, units).unit + ); - public state: State = valueToState(this.props.value, this.props.units!); - - public componentWillReceiveProps(nextProps: Props) { - this.setState( - valueToState(nextProps.value, this.props.units!, this.state.unit) - ); - } - - private handleValueChange = (e: ChangeEvent) => { - if (this.props.onChange) { - const newState: State = { - ...this.state, - value: e.target.value, - }; - // Assume we have a controlled component and propage the value up, - // it then should come back in the props. - this.props.onChange(stateToValue(newState)); + // If value changes, and selectedUnit has not been set, then set the value. + useEffect(() => { + if (!selectedUnit) { + setSelectedUnit(convertFromSeconds(value, units).unit); } - }; + }, [value]); - private handleUnitChange = (e: ChangeEvent) => { - // First set new unit before propagting new value. - this.setState( - { - unit: parseInt(e.target.value, 10), - }, - () => { - if (this.props.onChange) { - this.props.onChange(stateToValue(this.state)); - } - } - ); - }; + const elementCallbacks = useMemo(() => units.map(k => DURATION_UNIT_MAP[k]), [ + units, + ]); - public render() { - const { disabled, name } = this.props; + const { value: computedValue } = useMemo( + () => convertFromSeconds(value, units, selectedUnit), + [value, selectedUnit] + ); - if (!this.state.elementCallbacks) { - return null; - } + const handleValueChange = useCallback( + (e: ChangeEvent) => { + onChange(convertToSeconds(e.target.value, selectedUnit)); + }, + [onChange, selectedUnit] + ); - return ( - - ) => { + const unit = parseInt(e.target.value, 10); + setSelectedUnit(unit); + onChange(convertToSeconds(computedValue, unit)); + }, + [setSelectedUnit, onChange, computedValue] + ); + + return ( + + + {elementCallbacks.length === 1 ? ( + + + {elementCallbacks[0]} + + + ) : ( + - {this.state.elementCallbacks.length === 1 ? ( - - - {this.state.elementCallbacks[0]} - - - ) : ( - - {this.state.elementCallbacks.map((unit, i) => { - const value = this.state.units[i]; - return ( - - - - ); - })} - - )} - - ); - } -} + aria-label="unit" + classes={{ + select: styles.select, + }} + value={(selectedUnit || units[0]).toString()} + > + {elementCallbacks.map((unit, i) => { + const val = units[i]; + return ( + + + + ); + })} + + )} + + ); +}; export default DurationField;