Skip to content

Commit

Permalink
draft(Forms): API draft for how fields can handle API connections
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Dec 18, 2024
1 parent d2f5c23 commit c78b161
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import classnames from 'classnames'
import { Props as FieldBlockProps } from '../../FieldBlock'
import StringField, { Props as StringFieldProps } from '../String'
import CompositionField from '../Composition'
import { Path } from '../../types'
import { ConnectorProps, Path } from '../../types'
import useTranslation from '../../hooks/useTranslation'
import useDataValue from '../../hooks/useDataValue'
import useConnector from '../../hooks/useConnector'
import { COUNTRY as defaultCountry } from '../../../../shared/defaults'
import { HelpProps } from '../../../../components/help-button/HelpButtonInline'

Expand All @@ -22,6 +23,7 @@ export type Props = Pick<
*/
country?: Path | string
help?: HelpProps
connector?: Record<'postalCode' | 'city', ConnectorProps<string>>
}

function PostalCodeAndCity(props: Props) {
Expand All @@ -34,9 +36,16 @@ function PostalCodeAndCity(props: Props) {
help,
width = 'large',
country = defaultCountry,
connector,
...fieldBlockProps
} = props

const postalCodeConnector = useConnector<string>(
connector?.postalCode,
props
)
const cityConnector = useConnector<string>(connector?.city, props)

const {
pattern: cityPattern,
className: cityClassName,
Expand Down Expand Up @@ -98,6 +107,8 @@ function PostalCodeAndCity(props: Props) {
mask={postalCodeMask}
pattern={postalCodePattern}
placeholder={postalCodePlaceHolder}
onChange={postalCodeConnector?.onChange}
onBlurValidator={postalCodeConnector?.onBlurValidator}
errorMessages={useMemo(
() => ({
'Field.errorRequired': translations.PostalCode.errorRequired,
Expand All @@ -124,6 +135,8 @@ function PostalCodeAndCity(props: Props) {
cityClassName
)}
label={cityLabel ?? translations.City.label}
onChange={cityConnector?.onChange}
onBlurValidator={cityConnector?.onBlurValidator}
errorMessages={useMemo(
() => ({
'Field.errorRequired': translations.City.errorRequired,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Field, Form, Value } from '../../..'
import { Flex } from '../../../../../components'

export default {
title: 'Eufemia/Extensions/Forms/PostalCodeAndCity',
Expand Down Expand Up @@ -50,3 +51,147 @@ export function PostalCodeAndCityCountryCodeSelection() {
</>
)
}

/**
* The idea is to add a new property called "connector" to every field that can be used with the Bring API.
* A connector allows a field to both support events like before, but also events from the connector.
* A connector is essentially a plugin that can be used to add support for APIs like Bring.
* Eufemia Forms delivers a default connector that devs can use with their own APIs and tokens etc.
*
* This is a draft on how we can deliver a flexible Bring API connection.
* It should be possible to customize / replace:
*
* - fetch routine
* - maybe it always should be defined?
* - how properties are mapped get and set
* - API url and headers
* - add a plugin system, so we can add support for other APIs than Bring
*
*/

/**
* This is a part of Eufemia:
*/
const External = {
BringConnector: {
postalCodeAndCity: (apiConnectionConfig = null) => {
return {
postalCode: {
// onChange: async (value) => {
// await new Promise((resolve) => setTimeout(resolve, 1000))
// },
onBlurValidator: async (value) => {
const data = await verifyPostalCodeByAPI(value)
if (data.postal_codes[0].postal_code !== value) {
return new Error('💁‍♂️ Feil i postnummeret')
}
},
},
city: {
// onChange: async (value) => {
// await new Promise((resolve) => setTimeout(resolve, 1000))
// },
},
}
},
},
}

export function PostalCodeAPI_Draft() {
return (
<Form.Handler defaultData={{ postalCode: '123', city: 'Oslo' }}>
{/* <Field.PostalCodeAndCity
// import { External } from '@dnb/eufemia/extensions/forms'
// connector={External.BringConnector.postalCodeAndCity()}
postalCode={{
path: '/postalCode',
// onChange: async (value) => {
// await new Promise((resolve) => setTimeout(resolve, 1000))
// console.log('onChange', value)
// },
onBlurValidator: async (value) => {
console.log('value', value)
await new Promise((resolve) => setTimeout(resolve, 2000))
return new Error('💁‍♂️ Feil i postnummeret')
},
validateInitially: true,
}}
city={{ path: '/city' }}
/> */}

<Field.PostalCodeAndCity
postalCode={{
placeholder: '????',
onChange: (value) => console.log('postalCode onChange', value),
}}
city={{
placeholder: 'Your city',
onChange: (value) => console.log('city onChange', value),
}}
/>
</Form.Handler>
)
}

async function verifyPostalCodeByAPI(postalCode: string) {
await new Promise((resolve) => setTimeout(resolve, 600))

const mockData = {
city: 'Vollen',
county: 'Akershus',
latitude: '59.78899739297151',
longitude: '10.482494731266165',
municipality: 'Asker',
municipalityId: '3203',
po_box: false,
postal_code: '1391',
}
return { postal_codes: [mockData] }

// Visit: https://cors-anywhere.herokuapp.com/corsdemo to enable this service
const url = `https://cors-anywhere.herokuapp.com/https://api.bring.com/address/api/no/postal-codes/${postalCode}`
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
'X-Mybring-API-Uid': '',
'X-Mybring-API-Key': '',
},
})

return await response.json()
}

// // This is a config for a Bring Plugin
// const apiConnectionConfig = {
// // Optional
// connections: {
// // Optional
// postalCode: {
// // Optional
// url: ({ postalCode }) =>
// `https://api.bring.com/address/api/no/postal-codes/${postalCode}`,
// // Optional
// // headers: {
// // 'X-Mybring-API-Uid': '',
// // },
// },
// },
// // Optional
// sharedBetweenAllConnections: {
// headers: {
// 'X-Mybring-API-Uid': '',
// },
// },
// // Optional
// // fetch: async ({ url }) => {
// // return await fetch(url, {
// // method: 'GET',
// // headers: {
// // Accept: 'application/json',
// // 'X-Mybring-API-Uid': '',
// // 'X-Mybring-API-Key': '',
// // },
// // })
// // },
// }
101 changes: 101 additions & 0 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { isAsync } from '../../../shared/helpers/isAsync'
import { ConnectorProps } from '../types'

import { useCallback } from 'react'

export default function useConnector<Value>(
connector: ConnectorProps<Value> = {},
props: Record<string, unknown> & ConnectorProps<Value> = {}
): {
onChange: ConnectorProps<Value>['onChange']
onBlurValidator: ConnectorProps<Value>['onBlurValidator']
} {
const { handler: onBlurValidator, asyncHandler: onBlurValidatorAsync } =
useCombinedHandlers(connector.onBlurValidator, props.onBlurValidator)
const { handler: onChange, asyncHandler: onChangeAsync } =
useCombinedHandlers(connector.onChange, props.onChange)

const onChangeRefs = [connector.onChange, props.onChange]
const onBlurValidatorRefs = [
connector.onBlurValidator,
props.onBlurValidator,
]

return {
onChange: isFunctionInArray(onChangeRefs)
? isAsyncInArray(onChangeRefs)
? onChangeAsync
: onChange
: undefined,
onBlurValidator: isFunctionInArray(onBlurValidatorRefs)
? isAsyncInArray(onBlurValidatorRefs)
? onBlurValidatorAsync
: onBlurValidator
: undefined,
}
}

function useCombinedHandlers(
connectorFn?: (...args: unknown[]) => unknown,
propsFn?: (...args: unknown[]) => unknown
) {
const handler = useCallback(
(...args: unknown[]) => {
{
const result = propsFn?.apply(this, args)
if (result) {
return result
}
}

{
const result = connectorFn?.apply(this, args)
if (result) {
return result
}
}
},
[connectorFn, propsFn]
)

const asyncHandler = useCallback(
async (...args: unknown[]) => {
{
const result = await propsFn?.apply(this, args)
if (result) {
return result
}
}

{
const result = await connectorFn?.apply(this, args)
if (result) {
return result
}
}
},
[connectorFn, propsFn]
)

return {
handler:
[connectorFn, propsFn].filter(Boolean).length > 0
? handler
: undefined,
asyncHandler:
[connectorFn, propsFn].filter(Boolean).length > 0
? asyncHandler
: undefined,
}
}

function isAsyncInArray(array: Array<unknown>) {
return array.some((fn) => {
return isAsync(fn)
})
}
function isFunctionInArray(array: Array<unknown>) {
return array.some((fn) => {
return typeof fn === 'function'
})
}
7 changes: 7 additions & 0 deletions packages/dnb-eufemia/src/extensions/forms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ export type DataValueReadWriteComponentProps<
DataValueReadProps<Value> &
DataValueWriteProps<Value, EmptyValue>

export type ConnectorProps<Value = unknown> = Pick<
UseFieldProps<Value>,
'onChange' | 'onBlurValidator'
>

export interface UseFieldProps<
Value = unknown,
EmptyValue = undefined | unknown,
Expand Down Expand Up @@ -400,6 +405,8 @@ export interface UseFieldProps<
* For internal use only.
*/
valueType?: string | number | boolean | Array<string | number | boolean>

connector?: ConnectorProps<Value>
}

export type FieldProps<
Expand Down

0 comments on commit c78b161

Please sign in to comment.