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 { useEffect } from 'react' |
||||||
|
|
||||||
import trim from 'lodash/trim' |
import trim from 'lodash/trim' |
||||||
|
|
||||||
import { useAuthStore } from 'features/AuthStore' |
import { useForm } from 'features/FormStore' |
||||||
import { isValidEmail } from 'features/Register/helpers/isValidEmail' |
import { isValidEmail } from 'features/Register/helpers/isValidEmail' |
||||||
|
|
||||||
import { formIds } from '../config' |
import { formIds } from '../config' |
||||||
|
import type { Country } from './useCountries' |
||||||
import { useCountries } from './useCountries' |
import { useCountries } from './useCountries' |
||||||
import { useCities } from './useCities' |
import { useCities } from './useCities' |
||||||
|
import { useSubmitHandler } from './useSubmitHandler' |
||||||
|
|
||||||
const readFormValue = (event: FormEvent<HTMLFormElement>) => ( |
export const useRegistrationForm = () => { |
||||||
(fieldName: string) => trim(event.currentTarget[fieldName]?.value) |
const { |
||||||
) |
readFormError, |
||||||
|
readFormValue, |
||||||
export const useForm = () => { |
updateFormError, |
||||||
const { register } = useAuthStore() |
updateFormValue, |
||||||
|
} = useForm() |
||||||
const { |
const { |
||||||
countries, |
countries, |
||||||
onCountrySelect, |
|
||||||
selectedCountry, |
selectedCountry, |
||||||
|
setSelectedCountry, |
||||||
} = useCountries() |
} = useCountries() |
||||||
|
|
||||||
const { |
const { |
||||||
cities, |
cities, |
||||||
cityQuery, |
getCities, |
||||||
onCityQueryChange, |
|
||||||
onCitySelect, |
onCitySelect, |
||||||
resetCities, |
resetCities, |
||||||
resetSelectedCity, |
resetSelectedCity, |
||||||
selectedCity, |
selectedCity, |
||||||
} = useCities(selectedCountry?.id) |
} = useCities() |
||||||
|
|
||||||
useEffect(() => { |
const handleSubmit = useSubmitHandler({ |
||||||
resetSelectedCity() |
selectedCity, |
||||||
resetCities() |
|
||||||
}, [ |
|
||||||
selectedCountry, |
selectedCountry, |
||||||
resetSelectedCity, |
}) |
||||||
resetCities, |
|
||||||
]) |
|
||||||
|
|
||||||
const getCityParams = () => { |
const onEmailBlur = ({ target }: FocusEvent<HTMLInputElement>) => { |
||||||
if (selectedCity) return { cityId: selectedCity.id } |
const email = trim(target.value) |
||||||
return { city: cityQuery } |
if (email && !isValidEmail(email)) { |
||||||
|
updateFormError(formIds.email, 'error_invalid_email_format') |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { |
const onCountrySelect = (country: Country | null) => { |
||||||
event.preventDefault() |
setSelectedCountry(country) |
||||||
if (!selectedCountry) return |
updateFormValue(formIds.country)(country?.name || '') |
||||||
|
resetCities() |
||||||
|
resetSelectedCity() |
||||||
|
} |
||||||
|
|
||||||
const readFieldValue = readFormValue(event) |
const onRegionOrCityChange = (fieldName: string) => ( |
||||||
const firstname = readFieldValue(formIds.firstname) |
({ target }: ChangeEvent<HTMLInputElement>) => { |
||||||
const lastname = readFieldValue(formIds.lastname) |
if (selectedCountry) { |
||||||
const phone = readFieldValue(formIds.phone) |
updateFormValue(fieldName)(target.value) |
||||||
const email = readFieldValue(formIds.email) |
} else { |
||||||
const password = readFieldValue(formIds.password) |
updateFormError(formIds.country, 'error_select_country_first') |
||||||
const postalCode = Number(readFieldValue(formIds.postalCode)) |
} |
||||||
const region = readFieldValue(formIds.region) |
} |
||||||
const address1 = readFieldValue(formIds.address1) |
) |
||||||
const address2 = readFieldValue(formIds.address2) |
|
||||||
|
|
||||||
if (isValidEmail(email)) { |
const trimmedCity = trim(readFormValue(formIds.city)) |
||||||
register({ |
useEffect(() => { |
||||||
address1, |
if (trimmedCity && selectedCountry?.id) { |
||||||
address2, |
getCities(trimmedCity, selectedCountry.id) |
||||||
...getCityParams(), |
|
||||||
countryId: selectedCountry.id, |
|
||||||
email, |
|
||||||
firstname, |
|
||||||
lastname, |
|
||||||
password, |
|
||||||
phone, |
|
||||||
postalCode, |
|
||||||
region, |
|
||||||
}) |
|
||||||
} |
} |
||||||
} |
}, [ |
||||||
|
trimmedCity, |
||||||
|
selectedCountry, |
||||||
|
getCities, |
||||||
|
]) |
||||||
|
|
||||||
return { |
return { |
||||||
cities, |
cities, |
||||||
cityQuery, |
|
||||||
countries, |
countries, |
||||||
handleSubmit, |
handleSubmit, |
||||||
onCityQueryChange, |
|
||||||
onCitySelect, |
onCitySelect, |
||||||
onCountrySelect, |
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