diff --git a/src/app/address/AddressForm.tsx b/src/app/address/AddressForm.tsx index c77dd180c1..02d3c77bca 100644 --- a/src/app/address/AddressForm.tsx +++ b/src/app/address/AddressForm.tsx @@ -2,6 +2,7 @@ import { Address, Country, FormField } from '@bigcommerce/checkout-sdk'; import { forIn, noop } from 'lodash'; import React, { createRef, Component, ReactNode, RefObject } from 'react'; +import { memoize } from '../common/utility'; import { withLanguage, WithLanguageProps } from '../locale'; import { AutocompleteItem } from '../ui/autocomplete'; @@ -34,6 +35,10 @@ class AddressForm extends Component { private containerRef: RefObject = createRef(); private nextElement?: HTMLElement | null; + private handleDynamicFormFieldChange: (name: string) => (value: string | string[]) => void = memoize(name => value => { + this.syncNonFormikValue(name, value); + }, { maxSize: 0 }); + componentDidMount(): void { const { current } = this.containerRef; @@ -67,13 +72,9 @@ class AddressForm extends Component { countryCode={ countryCode } supportedCountries={ countriesWithAutocomplete } field={ field } - onSelect={ this.onAutocompleteSelect } + onSelect={ this.handleAutocompleteSelect } onToggleOpen={ onAutocompleteToggle } - onChange={ (value, isOpen) => { - if (!isOpen) { - this.syncNonFormikValue(AUTOCOMPLETE_FIELD_NAME, value); - } - } } + onChange={ this.handleAutocompleteChange } apiKey={ googleMapsApiKey } nextElement={ this.nextElement || undefined } /> @@ -82,7 +83,7 @@ class AddressForm extends Component { return ( this.syncNonFormikValue(addressFieldName, value) } + onChange={ this.handleDynamicFormFieldChange(addressFieldName) } // stateOrProvince can sometimes be a dropdown or input, so relying on id is not sufficient key={ `${field.id}-${field.name}` } parentFieldName={ field.custom ? @@ -129,7 +130,13 @@ class AddressForm extends Component { return fieldType as DynamicFormFieldType; } - private onAutocompleteSelect: ( + private handleAutocompleteChange: (value: string, isOpen: boolean) => void = (value, isOpen) => { + if (!isOpen) { + this.syncNonFormikValue(AUTOCOMPLETE_FIELD_NAME, value); + } + }; + + private handleAutocompleteSelect: ( place: google.maps.places.PlaceResult, item: AutocompleteItem ) => void = (place, { value: autocompleteValue }) => { diff --git a/src/app/address/AddressSelect.tsx b/src/app/address/AddressSelect.tsx index ae409b1c02..7103674fdf 100644 --- a/src/app/address/AddressSelect.tsx +++ b/src/app/address/AddressSelect.tsx @@ -1,5 +1,5 @@ import { Address, CustomerAddress } from '@bigcommerce/checkout-sdk'; -import React, { Component, FunctionComponent, ReactNode } from 'react'; +import React, { memo, FunctionComponent, PureComponent, ReactNode } from 'react'; import { preventDefault } from '../common/dom'; import { TranslatedString } from '../locale'; @@ -15,12 +15,11 @@ export interface AddressSelectProps { onUseNewAddress(currentAddress?: Address): void; } -class AddressSelect extends Component { +class AddressSelect extends PureComponent { render(): ReactNode { const { addresses, selectedAddress, - onUseNewAddress, } = this.props; return ( @@ -29,8 +28,8 @@ class AddressSelect extends Component { onUseNewAddress(selectedAddress) } + onSelectAddress={ this.handleSelectAddress } + onUseNewAddress={ this.handleUseNewAddress } selectedAddress={ selectedAddress } /> }> @@ -44,7 +43,7 @@ class AddressSelect extends Component { ); } - private onSelectAddress: (newAddress: Address) => void = (newAddress: Address) => { + private handleSelectAddress: (newAddress: Address) => void = (newAddress: Address) => { const { onSelectAddress, selectedAddress, @@ -54,6 +53,15 @@ class AddressSelect extends Component { onSelectAddress(newAddress); } }; + + private handleUseNewAddress: () => void = () => { + const { + selectedAddress, + onUseNewAddress, + } = this.props; + + onUseNewAddress(selectedAddress); + }; } const AddressSelectMenu: FunctionComponent = ({ @@ -102,4 +110,4 @@ const AddressSelectButton: FunctionComponent = ({ ); -export default AddressSelect; +export default memo(AddressSelect); diff --git a/src/app/address/CheckboxGroupFormField.tsx b/src/app/address/CheckboxGroupFormField.tsx index b4b8841676..8bbbc6a2d9 100644 --- a/src/app/address/CheckboxGroupFormField.tsx +++ b/src/app/address/CheckboxGroupFormField.tsx @@ -1,7 +1,7 @@ import { FormFieldItem } from '@bigcommerce/checkout-sdk'; -import { getIn, FieldArray } from 'formik'; -import { difference, kebabCase, noop } from 'lodash'; -import React, { FunctionComponent, ReactNode } from 'react'; +import { getIn, FieldArray, FieldArrayRenderProps } from 'formik'; +import { difference, kebabCase, noop, pick } from 'lodash'; +import React, { memo, useCallback, ChangeEvent, FunctionComponent, ReactNode } from 'react'; import { FormFieldContainer, FormFieldError } from '../ui/form'; @@ -10,66 +10,134 @@ import DynamicInput from './DynamicInput'; import MultiCheckboxControl from './MultiCheckboxControl'; export interface CheckboxGroupFormFieldProps { - name: string; id: string; label: ReactNode; + name: string; options: FormFieldItem[]; onChange?(values: string[]): void; } -const CheckboxGroupFormField: FunctionComponent = ({ +type MultiCheckboxFormFieldProps = ( + CheckboxGroupFormFieldProps & + Pick +); + +const MultiCheckboxFormField: FunctionComponent = ({ + form: { values, errors }, + id, label, name, + onChange = noop, + options, + pop, + push, + remove, +}) => { + const handleSelectAll = useCallback(() => { + const checkedValues: string[] = getIn(values, name) || []; + + difference(options.map(({ value }) => value), checkedValues) + .forEach(val => push(val)); + + onChange(getIn(values, name)); + }, [ + name, + onChange, + options, + push, + values, + ]); + + const handleSelectNone = useCallback(() => { + const checkedValues: string[] = getIn(values, name) || []; + + checkedValues.forEach(() => pop()); + + onChange(getIn(values, name)); + }, [ + name, + onChange, + pop, + values, + ]); + + const handleInputChange = useCallback((event: ChangeEvent) => { + const checkedValues: string[] = getIn(values, name) || []; + const { value, checked } = event.target; + + if (checked) { + push(value); + } else { + remove(checkedValues.indexOf(value)); + } + + onChange(getIn(values, name)); + }, [ + name, + onChange, + push, + remove, + values, + ]); + + return + { label } + + + + + + + ; +}; + +const CheckboxGroupFormField: FunctionComponent = ({ id, + label, + name, + onChange, options, - onChange = noop, -}) => ( - { + const renderField = useCallback((renderProps: FieldArrayRenderProps) => ( + + ), [ + id, + label, + name, + onChange, + options, + ]); + + return ( - - { label } - { - const checkedValues: string[] = getIn(values, name) || []; - difference(options.map(({ value }) => value), checkedValues) - .forEach(val => push(val)); - - onChange(getIn(values, name)); - }} - onSelectedNone={ () => { - const checkedValues: string[] = getIn(values, name) || []; - checkedValues.forEach(() => pop()); - onChange(getIn(values, name)); - }} - /> - { - const checkedValues: string[] = getIn(values, name) || []; - const { value, checked } = e.target; - - if (checked) { - push(value); - } else { - remove(checkedValues.indexOf(value)); - } - - onChange(getIn(values, name)); - } } - fieldType={ DynamicFormFieldType.checkbox } - options={ options } - id={ id } - /> - - - )} - /> -); + render={ renderField } + />; +}; -export default CheckboxGroupFormField; +export default memo(CheckboxGroupFormField); diff --git a/src/app/address/DynamicFormField.tsx b/src/app/address/DynamicFormField.tsx index ade8236f8d..3d0f93c47b 100644 --- a/src/app/address/DynamicFormField.tsx +++ b/src/app/address/DynamicFormField.tsx @@ -1,5 +1,6 @@ import { FormField as FormFieldType } from '@bigcommerce/checkout-sdk'; -import React, { FunctionComponent } from 'react'; +import { FieldProps } from 'formik'; +import React, { memo, useCallback, useMemo, FunctionComponent } from 'react'; import { TranslatedString } from '../locale'; import { FormField, Label } from '../ui/form'; @@ -74,7 +75,8 @@ const DynamicFormField: FunctionComponent = ({ const fieldInputId = getFormFieldInputId(addressFieldName); const fieldName = parentFieldName ? `${parentFieldName}.${name}` : name; const translatedLabelString = LABEL[name]; - const label = ( + + const label = useMemo(() => ( - ); + ), [ + custom, + fieldInputId, + fieldLabel, + required, + translatedLabelString, + ]); + + const renderInput = useCallback(({ field }: FieldProps) => ( + + ), [ + addressFieldName, + fieldInputId, + fieldType, + max, + maxLength, + min, + options, + placeholder, + ]); return (
@@ -103,25 +135,12 @@ const DynamicFormField: FunctionComponent = ({ label } - input={ props => - - } + label={ label } + input={ renderInput } /> }
); }; -export default DynamicFormField; +export default memo(DynamicFormField); diff --git a/src/app/address/DynamicInput.tsx b/src/app/address/DynamicInput.tsx index 64f0bdffe9..580f11a8e9 100644 --- a/src/app/address/DynamicInput.tsx +++ b/src/app/address/DynamicInput.tsx @@ -1,6 +1,6 @@ import { FormFieldItem } from '@bigcommerce/checkout-sdk'; -import { isDate } from 'lodash'; -import React, { FunctionComponent } from 'react'; +import { isDate, noop } from 'lodash'; +import React, { memo, useCallback, FunctionComponent } from 'react'; import ReactDatePicker from 'react-datepicker'; import { CheckboxInput, InputProps, RadioInput, TextArea, TextInput } from '../ui/form'; @@ -19,17 +19,32 @@ export interface DynamicInputProps extends InputProps { const DynamicInput: FunctionComponent = ({ additionalClassName, fieldType, + id, + name, + onChange = noop, options, placeholder, value, - id, ...rest }) => { + const handleDateChange = useCallback((date, event) => onChange({ + ...event, + target: { + name, + value: date, + }, + }), [ + onChange, + name, + ]); + switch (fieldType) { case DynamicFormFieldType.dropdown: return (