Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert AddressForm From Angular to React #969

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ module.exports = {
'jest-date-mock',
'<rootDir>/jest/setup.js'
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^.+\\.(css|scss)$': '<rootDir>/__mocks__/styleMock.js'
},
modulePaths: [
'<rootDir>/src'
],
transform: {
'^.+\\.js?$': 'babel-jest',
'\\.[jt]sx?$': 'babel-jest',
'^.+\\.html$': '<rootDir>/jest/htmlTransform.js'
}
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"change-case-object": "^2.0.0",
"cru-payments": "^1.2.2",
"crypto-js": "^3.1.9-1",
"formik": "^2.2.9",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.11",
"moment": "^2.24.0",
Expand All @@ -31,7 +32,8 @@
"rxjs": "^5.2.0",
"slick-carousel": "1.8.1",
"textangularjs": "^2.1.2",
"typescript": "^4.5.5"
"typescript": "^4.5.5",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
Expand Down Expand Up @@ -64,7 +66,6 @@
"standard": "^16.0.4",
"style-loader": "^1.0.0",
"ts-essentials": "^9.1.2",
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"typescript-eslint": "^0.0.1-alpha.0",
"webpack": "^4.39.2",
Expand Down
305 changes: 305 additions & 0 deletions src/common/components/addressForm/addressForm.react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import React, { useEffect, useState } from 'react';
import angular from 'angular';
import { react2angular } from 'react2angular';
import { Formik } from 'formik';
import * as Yup from 'yup';
import find from 'lodash/find';

import CountrySelect from './countrySelect';
import RegionSelect from './regionSelect';
import TextInput from '../form/textInput';
import FormikAutoSave from '../form/formikAutoSave';

interface AddressFormProps {
address: Address,
addressDisabled?: boolean,
onAddressChanged: (updatedAddress: Address) => void,
geographiesService: any,
$log: any
}

export interface Address {
country: string,
locality: string,
region: string,
postalCode: string,
streetAddress: string,
extendedAddress?: string,
intAddressLine3?: string,
intAddressLine4?: string
}

interface GeographiesLink {
href: string,
rel: string,
type: string,
uri: string,
}

interface GeographiesItem {
"display-name": string,
links: GeographiesLink[],
name: string,
}

const componentName = 'reactAddressForm';

const AddressForm = ({
address,
addressDisabled = false,
onAddressChanged,
geographiesService,
$log
}: AddressFormProps) => {

const [countryName, setCountryName] = useState<string | undefined>(address.country);
const [countries, setCountries] = useState<GeographiesItem[]>([]);
const [regions, setRegions] = useState<GeographiesItem[]>([]);

const [loadingCountriesError, setLoadingCountriesError] = useState<boolean>(false);
const [loadingRegionsError, setLoadingRegionsError] = useState<boolean>(false);

useEffect(() => {
loadCountries();
}, []);

const dropdownSortComparator = (a: GeographiesItem, b: GeographiesItem) => {
if(a['display-name'] < b['display-name']) return -1;
if(a['display-name'] > b['display-name']) return 1;
return 0;
};

const AddressSchema = Yup.object().shape({
country: Yup.string()
.required('You must select a country'),
streetAddress: Yup.string()
.max(200, 'This field cannot be longer than 200 characters')
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this use the maxLength property instead of hard-coded 200?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think the maxLength property on the input itself is accessible in this scope.

What I've also noticed is that setting maxLength on the input field prevents the user from entering any more characters than that limit. So I don't think this error will even show up at all. I think the input on its own does a good job of preventing the max length from being exceeded.

.required('You must enter an address'),
extendedAddress: Yup.string()
.max(100, 'This field cannot be longer than 100 characters'),
intAddressLine3: Yup.string()
.max(100, 'This field cannot be longer than 100 characters'),
intAddressLine4: Yup.string()
.max(100, 'This field cannot be longer than 100 characters'),
locality: Yup.string()
.max(50, 'This field cannot be longer than 100 characters')
.required('You must enter a city'),
region: Yup.string()
.required('You must select a state / region'),
postalCode: Yup.string()
.test(
'is-postal-code',
() => 'You must enter a valid US zip code',
(value) => value == null || /^\d{5}(?:[-\s]\d{4})?$/.test(value)
)
.required('You must enter a zip / postal code')
});

const handleAddressChanged = (values: Address) => {
onAddressChanged(values);
};

const loadCountries = () => {
setLoadingCountriesError(false);

geographiesService.getCountries()
.subscribe((data: GeographiesItem[]) => {
const sortedCountries = data.sort(dropdownSortComparator);

setCountries(sortedCountries);

const countryContext = countryName && findCountry(sortedCountries, countryName);
countryContext && loadRegions(countryContext);
},
(error: any) => {
setLoadingCountriesError(true);
$log.error('Error loading countries.', error);
});
};

const loadRegions = (countryContext: GeographiesItem) => {
setLoadingRegionsError(false);

geographiesService.getRegions(countryContext)
.subscribe((data: GeographiesItem[]) => {
const sortedRegions = data.sort(dropdownSortComparator);

setRegions(sortedRegions);
},
(error: any) => {
setLoadingRegionsError(true);
$log.error('Error loading regions.', error);
});
};

const findCountry = (countryOptions: GeographiesItem[], countryName?: string): GeographiesItem | undefined => {
let foundCountry: GeographiesItem | undefined = undefined;

if (countryOptions.length > 0) {
foundCountry = find(countryOptions, { name: countryName });
}

return foundCountry;
};

const refreshRegions = () => {
const countryContext = countryName && findCountry(countries, countryName);
countryContext && loadRegions(countryContext);
}

return (
<Formik
initialValues={address}
validationSchema={AddressSchema}
onSubmit={handleAddressChanged}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
}) => (
<>
<FormikAutoSave debounceMs={600} />
<div className="row">
<div className="col-sm-12">
<CountrySelect
addressDisabled={addressDisabled}
countries={countries.map(country => ({ name: country.name, displayName: country['display-name']}))}
onChange={handleChange}
Copy link
Contributor

Choose a reason for hiding this comment

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

This should call refreshRegions I think. Not seeing how that is being done here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I noticed that too, I'll push something that should address that.

onBlur={handleBlur}
onSelectCountry={setCountryName}
refreshCountries={loadCountries}
value={values.country}
error={loadingCountriesError
? 'There was an error loading the list of countries. If you continue to see this message, contact <a href="mailto:[email protected]">[email protected]</a> for assistance.'
: touched.country && errors.country
? errors.country
: undefined
}
canRetry={loadingCountriesError}
/>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<TextInput
title="Address"
name="streetAddress"
required
maxLength={200}
disabled={addressDisabled}
onChange={handleChange}
onBlur={handleBlur}
value={values.streetAddress}
error={touched.streetAddress && errors.streetAddress || undefined}
/>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<TextInput
name="extendedAddress"
maxLength={100}
disabled={addressDisabled}
onChange={handleChange}
onBlur={handleBlur}
value={values.extendedAddress}
error={touched.extendedAddress && errors.extendedAddress || undefined}
/>
</div>
</div>
{
countryName && countryName !== 'US'
? (
<>
<div className="row">
<div className="col-sm-12">
<TextInput
name="intAddressLine3"
maxLength={100}
disabled={addressDisabled}
onChange={handleChange}
onBlur={handleBlur}
value={values.intAddressLine3}
error={touched.intAddressLine3 && errors.intAddressLine3 || undefined}
/>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<TextInput
name="intAddressLine4"
maxLength={100}
disabled={addressDisabled}
onChange={handleChange}
onBlur={handleBlur}
value={values.intAddressLine4}
error={touched.intAddressLine4 && errors.intAddressLine4 || undefined}
/>
</div>
</div>
</>
) : (
<>
<div className="row">
<div className="col-sm-12">
<TextInput
title="City"
name="locality"
required
maxLength={50}
disabled={addressDisabled}
onChange={handleChange}
onBlur={handleBlur}
value={values.locality}
error={touched.locality && errors.locality || undefined}
/>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<RegionSelect
addressDisabled={addressDisabled}
regions={regions.map(region => ({ name: region.name, displayName: region['display-name']}))}
onChange={handleChange}
onBlur={handleBlur}
refreshRegions={refreshRegions}
value={values.region}
error={loadingRegionsError
? 'There was an error loading the list of regions/state. If you continue to see this message, contact <a href="mailto:[email protected]">[email protected]</a> for assistance.'
: touched.region && errors.region
? errors.region
: undefined
}
canRetry={loadingRegionsError}
/>
</div>
<div className="col-sm-6">
<TextInput
title="Zip / Postal Code"
name="postalCode"
required
disabled={addressDisabled}
onChange={handleChange}
onBlur={handleBlur}
value={values.postalCode}
error={touched.postalCode && errors.postalCode || undefined}
/>
</div>
</div>
</>
)
}
</>
)}
</Formik>
);
};

export default angular
.module(componentName, [])
.component(componentName, react2angular(AddressForm, ['address', 'addressDisabled', 'onAddressChanged'], ['geographiesService', '$log']))

export { AddressForm }
Loading