Skip to content

Commit

Permalink
error state handling WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeKarow committed Sep 8, 2023
1 parent 7d7724c commit a097aa0
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const HookFormContextDecorator = (Story: StoryFn) => {
const form = useHookForm<HookFormParams>()
return (
<FormProvider {...form}>
<Story />
<form>
<Story />
</form>
</FormProvider>
)
}
Expand All @@ -33,6 +35,7 @@ export default {
component: PhoneNumberEntry,
parameters: {
msw: [fieldOpt.countries],
rqDevtools: true,
},
} satisfies Meta<typeof PhoneNumberEntry>

Expand Down
134 changes: 97 additions & 37 deletions packages/ui/components/data-portal/PhoneNumberEntryHookForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { createStyles, Group, rem, Text } from '@mantine/core'
import { AsYouType, type CountryCode } from 'libphonenumber-js'
import { type ComponentPropsWithoutRef, forwardRef, useEffect, useState } from 'react'
import { type FieldValues, useFormContext, useWatch } from 'react-hook-form'
import { Select, type SelectProps, TextInput, type TextInputProps } from 'react-hook-form-mantine'
import PhoneInput, { parsePhoneNumber, type Props as PhoneInputProps } from 'react-phone-number-input/input'
import { type SetOptional } from 'type-fest'
import { DevTool } from '@hookform/devtools'
import { ErrorMessage } from '@hookform/error-message'
import { createStyles, Group, rem, Text, TextInput, type TextInputProps } from '@mantine/core'
import { AsYouType } from 'libphonenumber-js'
import { type ComponentPropsWithoutRef, forwardRef, useEffect, useMemo } from 'react'
import { type FieldValues, type Path, useFormContext, useFormState } from 'react-hook-form'
import {
Select,
type SelectProps,
// TextInput,
// type TextInputProps,
} from 'react-hook-form-mantine'
import PhoneInput, {
// parsePhoneNumber,
type Props as PhoneInputProps,
} from 'react-phone-number-input/react-hook-form-input'

import { type ApiOutput } from '@weareinreach/api'
import { isCountryCode } from '~ui/hooks/usePhoneNumber'
Expand All @@ -16,8 +25,10 @@ const PhoneCountrySelectItem = forwardRef<HTMLDivElement, PhoneCountrySelectItem
({ data, value, label, ...props }, ref) => {
const { name } = data
return (
<Group ref={ref} {...props} w='100%'>
<Text>{`${label} ${name}`}</Text>
<Group ref={ref} {...props} w='100%' noWrap>
<Text>{label}</Text>
<Text>{name}</Text>
{/* <Text>{`${label} ${name}`}</Text> */}
</Group>
)
}
Expand All @@ -26,7 +37,7 @@ PhoneCountrySelectItem.displayName = 'PhoneCountrySelectItem'

const useCountrySelectStyles = createStyles((theme) => ({
dropdown: {
width: 'fit-content !important',
width: 'max-content !important',
left: 'unset !important',
// right: 0,
},
Expand Down Expand Up @@ -57,16 +68,16 @@ export const PhoneNumberEntry = <T extends FieldValues>({
const form = useFormContext<T>()
// const selectedCountry = useWatch<T>({control: countrySelectProps.control, name: countrySelectProps.name})
const selectedCountry = form.watch(countrySelectProps.name)
const phoneNumber = form.watch(allPhoneEntryProps.name)
// const [selectedCountry, setSelectedCountry] = useState<CountryCode | undefined>()
const { classes: countrySelectClasses } = useCountrySelectStyles()
const { classes: phoneEntryClasses } = usePhoneEntryStyles()
const {
setError: setPhoneError,
value: phoneValue,
onChange: onPhoneChange,
// value: phoneValue,
// onChange: onPhoneChange,
...phoneEntryProps
} = allPhoneEntryProps

const topCountries = ['US', 'CA', 'MX']

const { data: countryList } = api.fieldOpt.countries.useQuery(
Expand All @@ -90,7 +101,13 @@ export const PhoneNumberEntry = <T extends FieldValues>({
}),
}
)
const phoneFormatter = new AsYouType(selectedCountry)
const activeCountry = useMemo(() => {
const result = countryList?.find(({ value }) => value === selectedCountry)?.data.cca2
if (result && isCountryCode(result)) return result
return undefined
}, [selectedCountry, countryList])

const phoneFormatter = new AsYouType(activeCountry)
// useEffect(() => {
// const { data } = countryList.find(({ value }) => value === countrySelectProps.value) ?? {}
// if (data?.cca2 && isCountryCode(data.cca2)) setSelectedCountry(data.cca2)
Expand All @@ -99,67 +116,110 @@ export const PhoneNumberEntry = <T extends FieldValues>({
// }, [countrySelectProps.value])

useEffect(() => {
if (phoneValue) {
phoneFormatter.input(phoneValue)
if (phoneNumber) {
phoneFormatter.input(phoneNumber)
const phoneCountry = phoneFormatter.getNumber()?.country
if (phoneCountry && phoneCountry !== selectedCountry) {

console.log(
`🚀 ~ file: PhoneNumberEntryHookForm.tsx:124 ~ useEffect ~ phoneCountry/selectedCountry:`,
phoneCountry,
selectedCountry
)
const formError = form.formState.errors
console.log(formError)
if ((!phoneCountry && !selectedCountry) || phoneCountry !== selectedCountry) {
const countryId = countryList.find(({ data }) => data.cca2 === phoneCountry)?.value

if (countryId) {
console.log('setting countryId')
// @ts-expect-error -> string is okay.
form.setValue(countrySelectProps.name, countryId)
if (countrySelectProps.onChange && typeof countrySelectProps.onChange === 'function') {
countrySelectProps.onChange(countryId)
}
} else if (typeof setPhoneError === 'function') {
setPhoneError(`Country not active: ${phoneCountry}`)
} else if (!phoneCountry && formError[phoneEntryProps.name]) {
console.log('clearing error')
form.resetField(countrySelectProps.name)
form.clearErrors(phoneEntryProps.name)
// // @ts-expect-error -> null is okay.
// form.setValue(countrySelectProps.name, null)
} else if (phoneCountry && !countryId) {
console.log('setting error')
form.setError(
phoneEntryProps.name,
{ message: `Country not active: ${phoneCountry}` },
{ shouldFocus: true }
)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [phoneValue])
}, [phoneNumber])

const countrySelection = (
<Select
data={countryList}
itemComponent={PhoneCountrySelectItem}
classNames={countrySelectClasses}
clearable
{...form.register(countrySelectProps.name)}
control={form.control}
{...countrySelectProps}
/>
)

return (
<PhoneInput
country={selectedCountry}
defaultCountry={DEFAULT_COUNTRY}
inputComponent={TextInput}
rightSection={countrySelection}
rightSectionWidth={56}
classNames={phoneEntryClasses}
value={parsePhoneNumber(String(phoneValue), DEFAULT_COUNTRY)?.number}
onChange={(e) => (onPhoneChange && typeof onPhoneChange === 'function' ? onPhoneChange(e) : undefined)}
{...form.register(phoneEntryProps.name)}
{...phoneEntryProps}
/>
<>
<PhoneInput
country={activeCountry}
defaultCountry={DEFAULT_COUNTRY}
inputComponent={TextInput}
rightSection={countrySelection}
rightSectionWidth={56}
classNames={phoneEntryClasses}
// value={parsePhoneNumber(String(phoneValue), DEFAULT_COUNTRY)?.number}
// onChange={(e) =>
// onPhoneChange && typeof onPhoneChange === 'function' ? onPhoneChange(e) : undefined
// }
control={form.control}
// error={form.getFieldState(phoneEntryProps.name).error?.message}
// rules={{
// validate: (val) => {
// if (val) {
// const formatter = phoneFormatter.input(val)
// console.log('formatter', formatter)
// }
// const derivedCountry = phoneFormatter.getNumber()?.country
// console.log('val', val)
// console.log('derivedCountry', derivedCountry)
// const result = !!countryList.find(({ data }) => data.cca2 === phoneFormatter.getNumber()?.country)
// console.log('validator result', result)
// return result
// },
// }}
// @ts-expect-error -> name prop is okay.
error={<ErrorMessage errors={form.formState.errors} name={phoneEntryProps.name} as='span' />}
{...phoneEntryProps}
/>
<DevTool control={form.control} />
</>
)
}

export interface PhoneNumberEntryProps<T extends FieldValues> {
countrySelectProps: Omit<SelectProps<T>, 'data' | 'itemComponent' | 'classNames' | 'clearable'>
phoneEntryProps: Omit<
SetOptional<
PhoneInputProps<Omit<TextInputProps<T>, 'rightSection' | 'rightSectionWidth' | 'classNames'>>,
'onChange'
>,
PhoneInputProps<TextInputProps, T>,
'country' | 'defaultCountry' | 'itemComponent'
> & {
setError?: (err: string) => void
'data-autofocus'?: boolean
name: Path<T>
}
hookForm?: boolean
}

type CountryList = ApiOutput['fieldOpt']['countries']

interface PhoneCountrySelectItem extends ComponentPropsWithoutRef<'div'>, PhoneCountryItem {}
interface PhoneCountryItem {
label: string
Expand Down

0 comments on commit a097aa0

Please sign in to comment.