Ott 167 validation errors (#38)
parent
b592faea5f
commit
5bfa0d7b7e
@ -0,0 +1,64 @@ |
||||
import type { ChangeEvent } from 'react' |
||||
import { useState, useCallback } from 'react' |
||||
|
||||
import isString from 'lodash/isString' |
||||
|
||||
type FieldState = { |
||||
error: string | null, |
||||
value: string, |
||||
} |
||||
|
||||
type FormState = {[formId: string]: FieldState} |
||||
|
||||
export const useFormState = () => { |
||||
const [formState, setFormState] = useState<FormState>({}) |
||||
const readFormValue = useCallback( |
||||
(fieldName: string) => formState[fieldName]?.value ?? '', |
||||
[formState], |
||||
) |
||||
const readFormError = useCallback( |
||||
(fieldName: string) => formState[fieldName]?.error ?? null, |
||||
[formState], |
||||
) |
||||
|
||||
const updateFormValue = useCallback( |
||||
(fieldName: string) => ( |
||||
(event: ChangeEvent<HTMLInputElement> | string) => { |
||||
const value = isString(event) ? event : event.target.value |
||||
setFormState(((state) => { |
||||
const newState = { |
||||
...state, |
||||
[fieldName]: { |
||||
error: null, |
||||
value, |
||||
}, |
||||
} |
||||
return newState |
||||
})) |
||||
} |
||||
), |
||||
[], |
||||
) |
||||
|
||||
const updateFormError = useCallback( |
||||
(fieldName: string, error: string) => { |
||||
setFormState(((state) => { |
||||
const newState = { |
||||
...state, |
||||
[fieldName]: { |
||||
error, |
||||
value: state[fieldName]?.value, |
||||
}, |
||||
} |
||||
return newState |
||||
})) |
||||
}, |
||||
[], |
||||
) |
||||
return { |
||||
readFormError, |
||||
readFormValue, |
||||
updateFormError, |
||||
updateFormValue, |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@ |
||||
import trim from 'lodash/trim' |
||||
import every from 'lodash/every' |
||||
import isEmpty from 'lodash/isEmpty' |
||||
|
||||
import { useFormState } from './useFormState' |
||||
|
||||
type Args = ReturnType<typeof useFormState> |
||||
|
||||
export const useFormValidators = ({ |
||||
readFormValue, |
||||
updateFormError, |
||||
}: Args) => { |
||||
const isFieldEmpty = (fieldName: string) => isEmpty(trim(readFormValue(fieldName))) |
||||
|
||||
const allFieldsEmpty = (fieldNames: Array<string>) => ( |
||||
every(fieldNames, isFieldEmpty) |
||||
) |
||||
|
||||
return { |
||||
allFieldsEmpty, |
||||
isFieldEmpty, |
||||
} |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
import type { ReactNode } from 'react' |
||||
import React, { |
||||
createContext, |
||||
useContext, |
||||
useMemo, |
||||
} from 'react' |
||||
|
||||
import { useFormState } from './hooks/useFormState' |
||||
import { useFormValidators } from './hooks/useFormValidators' |
||||
|
||||
type FormStore = ( |
||||
ReturnType<typeof useFormState> |
||||
& ReturnType<typeof useFormValidators> |
||||
) |
||||
type Props = { |
||||
children: ReactNode, |
||||
} |
||||
|
||||
const FormContext = createContext({} as FormStore) |
||||
|
||||
/** |
||||
* стор формы, содержит состояние формы[значение полей, ошибки] |
||||
* дает доступ к функциям чтения и обновления состояния |
||||
* также часто используемым функциям для валидации полей |
||||
*/ |
||||
export const FormStore = ({ |
||||
children, |
||||
}: Props) => { |
||||
const formState = useFormState() |
||||
const validators = useFormValidators(formState) |
||||
const value = useMemo( |
||||
() => ({ ...formState, ...validators }), |
||||
[formState, validators], |
||||
) |
||||
return ( |
||||
<FormContext.Provider value={value}> |
||||
{children} |
||||
</FormContext.Provider> |
||||
) |
||||
} |
||||
|
||||
export const useForm = () => useContext(FormContext) |
||||
@ -1,91 +1,88 @@ |
||||
import type { FormEvent } from 'react' |
||||
import type { ChangeEvent, FocusEvent } from 'react' |
||||
import { useEffect } from 'react' |
||||
|
||||
import trim from 'lodash/trim' |
||||
|
||||
import { useAuthStore } from 'features/AuthStore' |
||||
import { useForm } from 'features/FormStore' |
||||
import { isValidEmail } from 'features/Register/helpers/isValidEmail' |
||||
|
||||
import { formIds } from '../config' |
||||
import type { Country } from './useCountries' |
||||
import { useCountries } from './useCountries' |
||||
import { useCities } from './useCities' |
||||
import { useSubmitHandler } from './useSubmitHandler' |
||||
|
||||
const readFormValue = (event: FormEvent<HTMLFormElement>) => ( |
||||
(fieldName: string) => trim(event.currentTarget[fieldName]?.value) |
||||
) |
||||
|
||||
export const useForm = () => { |
||||
const { register } = useAuthStore() |
||||
export const useRegistrationForm = () => { |
||||
const { |
||||
readFormError, |
||||
readFormValue, |
||||
updateFormError, |
||||
updateFormValue, |
||||
} = useForm() |
||||
const { |
||||
countries, |
||||
onCountrySelect, |
||||
selectedCountry, |
||||
setSelectedCountry, |
||||
} = useCountries() |
||||
|
||||
const { |
||||
cities, |
||||
cityQuery, |
||||
onCityQueryChange, |
||||
getCities, |
||||
onCitySelect, |
||||
resetCities, |
||||
resetSelectedCity, |
||||
selectedCity, |
||||
} = useCities(selectedCountry?.id) |
||||
} = useCities() |
||||
|
||||
useEffect(() => { |
||||
resetSelectedCity() |
||||
resetCities() |
||||
}, [ |
||||
const handleSubmit = useSubmitHandler({ |
||||
selectedCity, |
||||
selectedCountry, |
||||
resetSelectedCity, |
||||
resetCities, |
||||
]) |
||||
}) |
||||
|
||||
const getCityParams = () => { |
||||
if (selectedCity) return { cityId: selectedCity.id } |
||||
return { city: cityQuery } |
||||
const onEmailBlur = ({ target }: FocusEvent<HTMLInputElement>) => { |
||||
const email = trim(target.value) |
||||
if (email && !isValidEmail(email)) { |
||||
updateFormError(formIds.email, 'error_invalid_email_format') |
||||
} |
||||
} |
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { |
||||
event.preventDefault() |
||||
if (!selectedCountry) return |
||||
const onCountrySelect = (country: Country | null) => { |
||||
setSelectedCountry(country) |
||||
updateFormValue(formIds.country)(country?.name || '') |
||||
resetCities() |
||||
resetSelectedCity() |
||||
} |
||||
|
||||
const readFieldValue = readFormValue(event) |
||||
const firstname = readFieldValue(formIds.firstname) |
||||
const lastname = readFieldValue(formIds.lastname) |
||||
const phone = readFieldValue(formIds.phone) |
||||
const email = readFieldValue(formIds.email) |
||||
const password = readFieldValue(formIds.password) |
||||
const postalCode = Number(readFieldValue(formIds.postalCode)) |
||||
const region = readFieldValue(formIds.region) |
||||
const address1 = readFieldValue(formIds.address1) |
||||
const address2 = readFieldValue(formIds.address2) |
||||
const onRegionOrCityChange = (fieldName: string) => ( |
||||
({ target }: ChangeEvent<HTMLInputElement>) => { |
||||
if (selectedCountry) { |
||||
updateFormValue(fieldName)(target.value) |
||||
} else { |
||||
updateFormError(formIds.country, 'error_select_country_first') |
||||
} |
||||
} |
||||
) |
||||
|
||||
if (isValidEmail(email)) { |
||||
register({ |
||||
address1, |
||||
address2, |
||||
...getCityParams(), |
||||
countryId: selectedCountry.id, |
||||
email, |
||||
firstname, |
||||
lastname, |
||||
password, |
||||
phone, |
||||
postalCode, |
||||
region, |
||||
}) |
||||
const trimmedCity = trim(readFormValue(formIds.city)) |
||||
useEffect(() => { |
||||
if (trimmedCity && selectedCountry?.id) { |
||||
getCities(trimmedCity, selectedCountry.id) |
||||
} |
||||
} |
||||
}, [ |
||||
trimmedCity, |
||||
selectedCountry, |
||||
getCities, |
||||
]) |
||||
|
||||
return { |
||||
cities, |
||||
cityQuery, |
||||
countries, |
||||
handleSubmit, |
||||
onCityQueryChange, |
||||
onCitySelect, |
||||
onCountrySelect, |
||||
selectedCountry, |
||||
onEmailBlur, |
||||
onRegionOrCityChange, |
||||
readFormError, |
||||
readFormValue, |
||||
updateFormValue, |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,65 @@ |
||||
import type { FormEvent } from 'react' |
||||
|
||||
import trim from 'lodash/trim' |
||||
|
||||
import type { City } from 'requests' |
||||
|
||||
import { useAuthStore } from 'features/AuthStore' |
||||
import { useForm } from 'features/FormStore' |
||||
|
||||
import { formIds } from '../config' |
||||
import type { Country } from './useCountries' |
||||
import { useValidateForm } from './useValidateForm' |
||||
|
||||
type Args = { |
||||
selectedCity: City | null, |
||||
selectedCountry: Country | null, |
||||
} |
||||
|
||||
export const useSubmitHandler = ({ |
||||
selectedCity, |
||||
selectedCountry, |
||||
}: Args) => { |
||||
const { register } = useAuthStore() |
||||
const { readFormValue } = useForm() |
||||
const validateForm = useValidateForm() |
||||
|
||||
const readTrimmedValue = (fieldName: string) => trim(readFormValue(fieldName)) |
||||
|
||||
const getCityParams = () => { |
||||
if (selectedCity) return { cityId: selectedCity.id } |
||||
return { city: readTrimmedValue(formIds.city) } |
||||
} |
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { |
||||
event.preventDefault() |
||||
|
||||
if (validateForm() && selectedCountry) { |
||||
const firstname = readTrimmedValue(formIds.firstname) |
||||
const lastname = readTrimmedValue(formIds.lastname) |
||||
const phone = readTrimmedValue(formIds.phone) |
||||
const email = readTrimmedValue(formIds.email) |
||||
const password = readTrimmedValue(formIds.password) |
||||
const postalCode = Number(readTrimmedValue(formIds.postalCode)) |
||||
const region = readTrimmedValue(formIds.region) |
||||
const address1 = readTrimmedValue(formIds.address1) |
||||
const address2 = readTrimmedValue(formIds.address2) |
||||
|
||||
register({ |
||||
address1, |
||||
address2, |
||||
...getCityParams(), |
||||
countryId: selectedCountry.id, |
||||
email, |
||||
firstname, |
||||
lastname, |
||||
password, |
||||
phone, |
||||
postalCode, |
||||
region, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return handleSubmit |
||||
} |
||||
@ -0,0 +1,62 @@ |
||||
import trim from 'lodash/trim' |
||||
import reduce from 'lodash/reduce' |
||||
|
||||
import { useForm } from 'features/FormStore' |
||||
import { isValidEmail } from 'features/Register/helpers/isValidEmail' |
||||
import { isValidPassword } from 'features/Register/helpers/isValidPassword' |
||||
|
||||
import { |
||||
formIds, |
||||
requiredFields, |
||||
simpleValidationFields, |
||||
} from '../config' |
||||
|
||||
export const useValidateForm = () => { |
||||
const { |
||||
allFieldsEmpty, |
||||
isFieldEmpty, |
||||
readFormValue, |
||||
updateFormError, |
||||
} = useForm() |
||||
|
||||
const readTrimmedValue = (fieldName: string) => trim(readFormValue(fieldName)) |
||||
|
||||
const setErrorOnEmptyFields = (fieldNames: Array<string>, message: string) => ( |
||||
reduce( |
||||
fieldNames, |
||||
(acc, fieldName) => { |
||||
if (isFieldEmpty(fieldName)) { |
||||
updateFormError(fieldName, message) |
||||
return acc + 1 |
||||
} |
||||
return acc |
||||
}, |
||||
0, |
||||
) |
||||
) |
||||
|
||||
const validateForm = () => { |
||||
let errorsCount = 0 |
||||
const email = readTrimmedValue(formIds.email) |
||||
const password = readTrimmedValue(formIds.password) |
||||
if (allFieldsEmpty(requiredFields)) { |
||||
updateFormError(formIds.formError, 'error_fill_out_required_fields') |
||||
setErrorOnEmptyFields(requiredFields, '') |
||||
return false |
||||
} |
||||
if (isFieldEmpty(formIds.email)) { |
||||
updateFormError(formIds.email, 'error_empty_email') |
||||
errorsCount++ |
||||
} else if (!isValidEmail(email)) { |
||||
updateFormError(formIds.email, 'error_invalid_email_format') |
||||
} |
||||
if (!isValidPassword(password)) { |
||||
updateFormError(formIds.password, 'error_simple_password') |
||||
errorsCount++ |
||||
} |
||||
errorsCount += setErrorOnEmptyFields(simpleValidationFields, 'error_fill_out_this_field') |
||||
return errorsCount === 0 |
||||
} |
||||
|
||||
return validateForm |
||||
} |
||||
Loading…
Reference in new issue