Skip to content

Commit

Permalink
feat: add use-custom-element hook, extend form context, edit interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
absolemDev committed Nov 27, 2023
1 parent 84addff commit f8d6d3c
Show file tree
Hide file tree
Showing 19 changed files with 282 additions and 158 deletions.
1 change: 1 addition & 0 deletions packages/payment-widget/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './use-init.hook'
export { useValidate } from './use-validate.hook'
export { useFieldsState } from './use-fields-state.hook'
export { useFieldsRenderer } from './use-fields-render.hook'
export { useCustomElements } from './use-custom-elements.hook'
77 changes: 77 additions & 0 deletions packages/payment-widget/src/hooks/use-custom-elements.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Children } from 'react'
import { ReactNode } from 'react'
import { JSXElementConstructor } from 'react'
import { isValidElement } from 'react'

import { AdditionalFieldsType } from '../enums'
import { RequiredFieldsType } from '../enums'
import { CustomElements } from '../interfaces'
import { Field } from '../interfaces'

export const useCustomElements = (
existAmount: boolean,
existReceipt: boolean,
existAdditionalFields: boolean,
nodes: ReactNode
): CustomElements => {
const nodeArray = Children.toArray(nodes)

const isCastomElement = (nameNode: string, node: ReactNode): boolean =>
isValidElement(node)
? (node.type as JSXElementConstructor<any>).name === nameNode &&
typeof node.props.children === 'function'
: false
const isAdditionalField = (node: ReactNode): boolean =>
isValidElement(node) ? Object.values(AdditionalFieldsType).includes(node.props.name) : false
const isRequiredField = (node: ReactNode): boolean =>
isValidElement(node) ? node.props.name === RequiredFieldsType.Amount && !existAmount : false

const customFields = nodeArray.filter(
(node) =>
isCastomElement('InputWrapper', node) && (isAdditionalField(node) || isRequiredField(node))
)
const nameFields = customFields.reduce<Field[]>((acc, field) => {
if (isValidElement(field))
acc.push({
name: AdditionalFieldsType[field.props.name] || RequiredFieldsType[field.props.name],
})
return acc
}, [])

const customButton = nodeArray.find((node) => isCastomElement('ButtonWrapper', node))

const isGenerateReceiptField = existReceipt && !customFields.length

const isGenerateRequiredField = !existAmount && existAdditionalFields

const existCustomAmountField = customFields.some(
(field) => isValidElement(field) && field.props.name === RequiredFieldsType.Amount
)
const existCustomReceiptField = customFields.some(
(field) =>
isValidElement(field) &&
(field.props.name === AdditionalFieldsType.Email ||
field.props.name === AdditionalFieldsType.Phone)
)

if (existAdditionalFields && customFields.length)
throw new Error('Don`t use additionalFields property with InputWrapper component')

if (customFields.length && !existAmount && !existCustomAmountField)
throw new Error(
'If you use InputWrapper component and don`t set amount property, you mast use InputWrapper componnet with property name equal RequiredFieldsType.Amount'
)

if (customFields.length && existReceipt && !existCustomReceiptField)
throw new Error(
'If you set receipt property whith InputWrapper component, you mast use InputWrapper componnet with property name equal AdditionalFieldsType.Phone or AdditionalFieldsType.Email'
)

return {
customFields,
customButton,
isGenerateReceiptField,
isGenerateRequiredField,
nameFields,
}
}
52 changes: 29 additions & 23 deletions packages/payment-widget/src/hooks/use-fields-render.hook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import React from 'react'
import { useIntl } from 'react-intl'

import { Field } from '../interfaces'
import { NameField } from '../interfaces'
import { HandleBlurField } from '../interfaces'
import { FieldState } from '../interfaces'
import { HandleChangeField } from '../interfaces'
import { FieldsErrors } from '../interfaces'
import { MemoizedInput } from '../ui'
import { ThemeProvider } from '../ui/theme/src'
import { translate } from '../utils/translate.util'

export const useFieldsRenderer = (
fields: Field[],
fields: Field[] | NameField[],
errors: FieldsErrors,
fieldsState: FieldState,
handleChange: HandleChangeField,
Expand All @@ -22,27 +24,31 @@ export const useFieldsRenderer = (
) => {
const intl = useIntl()

return fields.map((field, index, currentFields) => {
const translatePlaceholder = translate(intl, field.placeholder, field.placeholder)
const translateError = translate(intl, errors[field.name], errors[field.name])
const isNotLastField = index !== currentFields.length - 1
return (
<ThemeProvider>
{fields.map((field, index, currentFields) => {
const translatePlaceholder = translate(intl, field.placeholder, field.placeholder)
const translateError = translate(intl, errors[field.name], errors[field.name])
const isNotLastField = index !== currentFields.length - 1

return (
<React.Fragment key={field.name}>
<MemoizedInput
type={field.type ?? 'text'}
name={field.name}
placeholder={translatePlaceholder}
required={field.required ?? false}
value={fieldsState[field.name]}
errorText={translateError}
onChangeNative={handleChange}
onBlur={handleBlur}
/>
<Condition match={isNotLastField}>
<Layout flexBasis={inputGaps} flexShrink={0} />
</Condition>
</React.Fragment>
)
})
return (
<React.Fragment key={field.name}>
<MemoizedInput
type={field.type ?? 'text'}
name={field.name}
placeholder={translatePlaceholder}
required={field.required ?? false}
value={fieldsState[field.name]}
errorText={translateError}
onChangeNative={handleChange}
onBlur={handleBlur}
/>
<Condition match={isNotLastField}>
<Layout flexBasis={inputGaps} flexShrink={0} />
</Condition>
</React.Fragment>
)
})}
</ThemeProvider>
)
}
13 changes: 4 additions & 9 deletions packages/payment-widget/src/hooks/use-fields-state.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,15 @@ import { HandleChangeField } from '../interfaces'
import { FieldsNames } from '../interfaces'
import { ValidateField } from '../interfaces'

export const useFieldsState = (validateField: ValidateField, fields?: Field[]): FieldsState => {
const initialState = fields
? fields.reduce((acc, field) => ({ ...acc, [field.name]: '' }), {})
: ''
const [fieldsState, setFieldsState] = useState<FieldState | string>(
initialState as FieldState | string
)
export const useFieldsState = (validateField: ValidateField, fields: Field[]): FieldsState => {
const initialState = fields.reduce((acc, field) => ({ ...acc, [field.name]: '' }), {})
const [fieldsState, setFieldsState] = useState<FieldState>(initialState as FieldState)

const handleChange: HandleChangeField = useCallback(
(e: FormEvent<HTMLInputElement>) => {
const name = e.currentTarget.name as FieldsNames
const { value } = e.currentTarget
setFieldsState((prevFields) =>
typeof prevFields === 'object' ? { ...prevFields, [name]: value } : value)
setFieldsState((prevFields) => ({ ...prevFields, [name]: value }))
},
[setFieldsState]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ReactNode } from 'react'

import { Field } from './fields.interfaces'

export type NameField = Pick<Field, 'name'>

export interface CustomElements {
customFields: ReactNode[]
customButton: ReactNode
isGenerateReceiptField: boolean
isGenerateRequiredField: boolean
nameFields: Field[]
}
7 changes: 3 additions & 4 deletions packages/payment-widget/src/interfaces/fields.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type HandleBlurField = (e: FormEvent<HTMLInputElement>) => void

export interface Field {
name: FieldsNames
placeholder: string
placeholder?: string
required?: boolean
type?: HTMLInputTypeAttribute
}
Expand All @@ -34,8 +34,7 @@ export interface FieldsProps {
amount?: number
direction?: DirectionFields
inputGaps?: number
isGenerateReceipt?: boolean
additionalFields?: AdditionalField[]
useCustomFields: boolean
}

export interface FieldsValidator {
Expand All @@ -45,7 +44,7 @@ export interface FieldsValidator {
}

export interface FieldsState {
fieldsState: FieldState | string
fieldsState: FieldState
handleChange: HandleChangeField
handleBlur: HandleBlurField
}
16 changes: 12 additions & 4 deletions packages/payment-widget/src/interfaces/form.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { ReactNode } from 'react'

import { NameField } from './custom-elements.iterfaces'
import { Field } from './fields.interfaces'
import { FieldsState } from './fields.interfaces'
import { FieldsValidator } from './fields.interfaces'
import { WidgetProps } from './widget.interfaces'

export interface FormProps extends WidgetProps {
useCustomFields: boolean
useCustomButton: boolean
isGenerateReceipt: boolean
children: ReactNode | ReactNode[]
}

export interface FormProviderProps {
additionalFields?: Field[]
nameFields: Field[]
isGenerateReceipt: boolean
isGenerateRequiredField: boolean
disabled: boolean
children: ReactNode | ReactNode[]
children: ReactNode
}

export interface FormContext extends FieldsValidator {
export interface FormContext extends FieldsValidator, FieldsState {
fields: Field[] | NameField[]
disabled: boolean
isLoaded: boolean
}
2 changes: 1 addition & 1 deletion packages/payment-widget/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export * from './settings.interfaces'
export * from './receipt.interfaces'
export * from './styles.interfaces'
export * from './theme.interfaces'
export * from './theme.interfaces'
export * from './form.interfaces'
export * from './wrappers.interfaces'
export * from './custom-elements.iterfaces'
1 change: 0 additions & 1 deletion packages/payment-widget/src/interfaces/theme.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ReactNode } from 'react'

export interface ThemeProps {
useCustomTheme: boolean
children: ReactNode
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export interface WidgetProps {
styles?: Styles
additionalFields?: AdditionalField[]
disabled?: boolean
children: ReactNode
children?: ReactNode
}
12 changes: 7 additions & 5 deletions packages/payment-widget/src/interfaces/wrappers.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ReactElement } from 'react'
import { ReactElement } from 'react'

import { ButtonType } from '../enums'
import { HandleBlurField } from './fields.interfaces'
import { HandleChangeField } from './fields.interfaces'
import { AdditionalFieldsType } from '../enums'
import { ButtonType } from '../enums'
import { RequiredFieldsType } from '../enums'
import { HandleBlurField } from './fields.interfaces'
import { HandleChangeField } from './fields.interfaces'

interface ChildrenInputProps {
name: string
Expand All @@ -18,7 +20,7 @@ interface ChildrenButtonProps {
}

export interface InputWrapperProps {
name: string
name: AdditionalFieldsType | RequiredFieldsType
children: (props: ChildrenInputProps) => ReactElement
}

Expand Down
45 changes: 19 additions & 26 deletions packages/payment-widget/src/ui/fields.component.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
import { Condition } from '@atls-ui-parts/condition'
import { HiddenInput } from '@atls-ui-parts/hidden-input'
import { Box } from '@atls-ui-parts/layout'
import { Column } from '@atls-ui-parts/layout'
import { Layout } from '@atls-ui-parts/layout'
import { Row } from '@atls-ui-parts/layout'
import { Condition } from '@atls-ui-parts/condition'
import { HiddenInput } from '@atls-ui-parts/hidden-input'
import { Box } from '@atls-ui-parts/layout'
import { Column } from '@atls-ui-parts/layout'
import { Layout } from '@atls-ui-parts/layout'
import { Row } from '@atls-ui-parts/layout'

import React from 'react'
import React from 'react'

import { RequiredFieldsType } from '../enums'
import { FieldState } from '../interfaces'
import { FieldsProps } from '../interfaces'
import { DirectionFields } from '../interfaces'
import { requiredFields } from '../data'
import { useFieldsState } from '../hooks'
import { useFieldsRenderer } from '../hooks'
import { addReceiptFieldsUtil } from '../utils'
import { useForm } from './form'
import { RequiredFieldsType } from '../enums'
import { FieldsProps } from '../interfaces'
import { DirectionFields } from '../interfaces'
import { useFieldsRenderer } from '../hooks'
import { useForm } from './form'

export const Fields = ({
amount,
additionalFields = [],
isGenerateReceipt = false,
direction = DirectionFields.Column,
inputGaps = 16,
useCustomFields,
}: FieldsProps) => {
const { errors, validateField } = useForm()
const processedFields = isGenerateReceipt
? addReceiptFieldsUtil(additionalFields)
: additionalFields
const fields = !amount ? [...requiredFields, ...processedFields] : [...processedFields]
const { fieldsState, handleChange, handleBlur } = useFieldsState(validateField, fields)
const { fields, fieldsState, handleChange, handleBlur, errors } = useForm()

const fieldsForRender = useCustomFields ? [] : fields

const renderedFields = useFieldsRenderer(
fields,
fieldsForRender,
errors,
fieldsState as FieldState,
fieldsState,
handleChange,
handleBlur,
inputGaps
Expand Down
Loading

0 comments on commit f8d6d3c

Please sign in to comment.