@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
390 lines (356 loc) • 12.5 kB
JavaScript
import { React, classnames } from '@gravityforms/libraries';
import { spacerClasses } from '@gravityforms/utils';
import Box from '../../elements/Box';
import Input from '../../elements/Input';
import Dropdown from '../Dropdown';
const { useState, useEffect, forwardRef } = React;
const NEEDS_I18N_LABEL = 'Needs i18n';
// Helper function to sort regions based on preferredRegions
const sortRegions = ( regions, preferred ) => {
if ( ! preferred || ! Array.isArray( preferred ) || preferred.length === 0 ) {
return [ ...regions ];
}
const preferredItems = [];
const otherItems = [];
regions.forEach( ( region ) => {
const index = preferred.indexOf( region.value );
if ( index !== -1 ) {
preferredItems[ index ] = region;
} else {
otherItems.push( region );
}
} );
// Filter out any undefined slots and combine with other items
return [
...preferredItems.filter( ( item ) => item !== undefined ),
...otherItems,
];
};
const transformRegionCodes = ( inputObj ) => {
const result = {};
for ( const key in inputObj ) {
if ( Object.prototype.hasOwnProperty.call( inputObj, key ) ) {
result[ key ] = Object.entries( inputObj[ key ] ).map( ( [ value, label ] ) => ( {
value,
label,
} ) );
}
}
return result;
};
/**
* @module Address
* @description Renders an address component.
*
* @since 5.5.0
*
* @param {React.ReactNode|React.ReactNode[]} [children] - React element children
* @param {Record<string, any>} [customAttributes] - Custom attributes for the component
* @param {string|string[]|Record<string, boolean>} [customClasses] - Custom classes for the component
* @param {Record<string, any>} [countryDropdownAttributes] - Attributes for the country dropdown
* @param {string|string[]|Record<string, boolean>} [countryDropdownClasses] - Classes for the country dropdown
* @param {Record<string, any>} [defaultData] - Default data for the component state
* @param {string[]} [disabledFields] - Fields to disable in the component. Use the state keys: lineOne, lineTwo, city, state, postalCode, country
* @param {Record<string, any>} [initialData] - Initial data for the component state
* @param {Record<string, any>} [i18n] - Internationalization settings
* @param {function} [onChange] - Callback for when the component changes, returns the state
* @param {boolean} [parseRegionCodes] - Whether to parse region codes for the component
* @param {Record<string, any>} [preferredRegions] - Preferred regions for the component: countries, usStates, caProvinces
* @param {Record<string, any>} [regionCodes] - Region codes for the component
* @param {string|number|string[]|Record<string, any>} [spacing=''] - The spacing for the component
* @param {Record<string, any>} [stateDropdownAttributes] - Attributes for the state/province dropdown
* @param {string|string[]|Record<string, boolean>} [stateDropdownClasses] - Classes for the state/province dropdown
* @param {boolean} [useSelectPlaceholders] - Whether to use select placeholders for the dropdowns
* @param {React.RefObject<HTMLElement>|null} ref - Ref to the component
*
* @return {JSX.Element} The Address component.
*
* @example
* import Address from '@gravityforms/components/react/admin/modules/Address';
*
* // Basic usage with all fields
* <Address
* regionCodes={{
* countries: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }],
* usStates: [{ value: 'CA', label: 'California' }, { value: 'NY', label: 'New York' }],
* caProvinces: [{ value: 'ON', label: 'Ontario' }, { value: 'QC', label: 'Quebec' }]
* }}
* onChange={(data) => console.log(data)}
* />
*
* // With preferred regions and initial data
* <Address
* regionCodes={{
* countries: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }],
* usStates: [{ value: 'CA', label: 'California' }, { value: 'NY', label: 'New York' }],
* caProvinces: [{ value: 'ON', label: 'Ontario' }, { value: 'QC', label: 'Quebec' }]
* }}
* preferredRegions={{
* countries: ['CA', 'US'],
* usStates: ['NY'],
* caProvinces: ['QC']
* }}
* initialData={{
* country: 'US',
* state: 'CA',
* city: 'San Francisco'
* }}
* />
*
* // With disabled fields
* <Address
* regionCodes={{
* countries: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }]
* }}
* disabledFields={['lineTwo', 'state']}
* initialData={{ country: 'US' }}
* />
*
* // With custom classes and spacing
* <Address
* regionCodes={{
* countries: [{ value: 'US', label: 'United States' }]
* }}
* customClasses={['custom-address-class']}
* spacing={4}
* customAttributes={{ 'data-test': 'address-component' }}
* >
* <div>Additional content</div>
* </Address>
*/
const Address = forwardRef( ( {
children = null,
customAttributes = {},
customClasses = [],
countryDropdownAttributes = {},
countryDropdownClasses = [],
defaultData = {},
disabledFields = [],
initialData = {},
i18n = {},
onChange = () => {},
parseRegionCodes = false,
preferredRegions = {},
regionCodes = {},
spacing = '',
stateDropdownAttributes = {},
stateDropdownClasses = [],
useSelectPlaceholders = true,
}, ref ) => {
const getInitialValue = ( field ) => {
if ( disabledFields.includes( field ) ) {
return undefined;
}
return initialData[ field ] || defaultData[ field ] || '';
};
const [ addressData, setAddressData ] = useState( () => (
[ 'lineOne', 'lineTwo', 'city', 'state', 'postalCode', 'country' ].reduce( ( carry, key ) => ( {
...carry,
[ key ]: getInitialValue( key ),
} ), {} )
) );
const { countries = [], usStates = [], caProvinces = [] } = parseRegionCodes ? transformRegionCodes( regionCodes ) : regionCodes;
const sortedCountries = sortRegions( countries, preferredRegions.countries );
const sortedUsStates = sortRegions( usStates, preferredRegions.usStates );
const sortedCaProvinces = sortRegions( caProvinces, preferredRegions.caProvinces );
useEffect( () => {
const filteredData = Object.fromEntries(
Object.entries( addressData ).filter( ( [ key ] ) => ! disabledFields.includes( key ) )
);
onChange( filteredData );
}, [ addressData ] );
const handleInputChange = ( field ) => ( value ) => {
if ( ! disabledFields.includes( field ) ) {
setAddressData( ( prev ) => ( {
...prev,
[ field ]: value,
} ) );
}
};
const handleDropdownChange = ( field ) => ( event, regionCode ) => {
if ( ! disabledFields.includes( field ) ) {
let options = [];
if ( field === 'country' ) {
options = sortedCountries;
} else if ( field === 'state' ) {
if ( addressData.country === 'US' ) {
options = sortedUsStates;
} else if ( addressData.country === 'CA' ) {
options = sortedCaProvinces;
}
}
const selected = options.find( ( opt ) => opt.value === regionCode.value );
setAddressData( ( prev ) => ( {
...prev,
[ field ]: selected ? selected.value : regionCode.value,
...( field === 'country' && ! disabledFields.includes( 'state' ) && { state: '' } ),
} ) );
}
};
const getStateLabelPlaceholder = () => {
if ( addressData.country === 'US' ) {
return {
label: i18n.stateLabel || NEEDS_I18N_LABEL,
placeholder: i18n.statePlaceholder || NEEDS_I18N_LABEL,
};
}
if ( addressData.country === 'CA' ) {
return {
label: i18n.provinceLabel || NEEDS_I18N_LABEL,
placeholder: i18n.provincePlaceholder || NEEDS_I18N_LABEL,
};
}
return {
label: i18n.stateProvinceLabel || NEEDS_I18N_LABEL,
placeholder: i18n.stateProvincePlaceholder || NEEDS_I18N_LABEL,
};
};
const getPostalCodeLabelPlaceholder = () => {
if ( addressData.country === 'US' ) {
return {
label: i18n.zipCodeLabel || NEEDS_I18N_LABEL,
placeholder: i18n.zipCodePlaceholder || NEEDS_I18N_LABEL,
};
}
if ( addressData.country === 'CA' ) {
return {
label: i18n.postalCodeLabel || NEEDS_I18N_LABEL,
placeholder: i18n.postalCodePlaceholder || NEEDS_I18N_LABEL,
};
}
return {
label: i18n.zipPostalCodeLabel || NEEDS_I18N_LABEL,
placeholder: i18n.zipPostalCodePlaceholder || NEEDS_I18N_LABEL,
};
};
const renderInputField = ( key ) => {
if ( disabledFields.includes( key ) ) {
return null;
}
let label = i18n[ `${ key }Label` ] || NEEDS_I18N_LABEL;
let placeholder = i18n[ `${ key }Placeholder` ] || NEEDS_I18N_LABEL;
if ( key === 'state' ) {
( { label, placeholder } = getStateLabelPlaceholder() );
} else if ( key === 'postalCode' ) {
( { label, placeholder } = getPostalCodeLabelPlaceholder() );
}
return <Input
key={ key }
labelAttributes={ { label } }
value={ addressData[ key ] }
onChange={ handleInputChange( key ) }
placeholder={ placeholder }
spacing={ 3 }
/>;
};
const renderCountryField = () => {
if ( disabledFields.includes( 'country' ) ) {
return null;
}
if ( sortedCountries.length === 0 ) {
return renderInputField( 'country' );
}
const listItems = useSelectPlaceholders ? [ {
label: i18n.countrySelectPlaceholder || NEEDS_I18N_LABEL,
value: '',
}, ...sortedCountries ] : sortedCountries;
const countryDropdownProps = {
controlled: true,
hasSearch: true,
i18n,
label: i18n.countryLabel || NEEDS_I18N_LABEL,
listItems,
onChange: handleDropdownChange( 'country' ),
popoverMaxHeight: 300,
size: 'r',
value: addressData.country,
...countryDropdownAttributes,
className: classnames( {
'gform-address__country-dropdown': true,
...countryDropdownClasses,
} ),
};
return (
<Dropdown { ...countryDropdownProps } />
);
};
const renderStateProvinceField = () => {
if ( disabledFields.includes( 'state' ) ) {
return null;
}
if ( addressData.country === 'US' && sortedUsStates.length > 0 ) {
const listItems = useSelectPlaceholders ? [ {
label: i18n.stateSelectPlaceholder || NEEDS_I18N_LABEL,
value: '',
}, ...sortedUsStates ] : sortedUsStates;
const stateDropdownProps = {
controlled: true,
hasSearch: true,
i18n,
label: i18n.stateLabel || NEEDS_I18N_LABEL,
listItems,
onChange: handleDropdownChange( 'state' ),
popoverMaxHeight: 300,
size: 'r',
value: addressData.state,
...stateDropdownAttributes,
className: classnames( {
'gform-address__state-province-dropdown': true,
...stateDropdownClasses,
} ),
};
return ( <Dropdown { ...stateDropdownProps } /> );
}
if ( addressData.country === 'CA' && sortedCaProvinces.length > 0 ) {
const listItems = useSelectPlaceholders ? [ {
label: i18n.provinceSelectPlaceholder || NEEDS_I18N_LABEL,
value: '',
}, ...sortedCaProvinces ] : sortedCaProvinces;
const stateDropdownProps = {
controlled: true,
hasSearch: true,
i18n,
label: i18n.provinceLabel || NEEDS_I18N_LABEL,
listItems,
onChange: handleDropdownChange( 'state' ),
popoverMaxHeight: 300,
size: 'r',
value: addressData.state,
...stateDropdownAttributes,
className: classnames( {
'gform-address__state-province-dropdown': true,
...stateDropdownClasses,
} ),
};
return (
<Dropdown { ...stateDropdownProps } />
);
}
return renderInputField( 'state' );
};
const attributes = {
className: classnames( {
'gform-address': true,
...spacerClasses( spacing ),
}, customClasses ),
ref,
...customAttributes,
};
return (
<div { ...attributes }>
{ renderInputField( 'lineOne' ) }
{ renderInputField( 'lineTwo' ) }
<Box display="flex" customClasses={ [ 'gform-address__row' ] }>
{ renderInputField( 'city' ) }
{ renderStateProvinceField() }
</Box>
<Box display="flex" customClasses={ [ 'gform-address__row' ] }>
{ renderInputField( 'postalCode' ) }
{ renderCountryField() }
</Box>
{ children }
</div>
);
} );
Address.displayName = 'Address';
export default Address;