From af26e9496ae65e72c75707cf186060158fa0ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 13 Dec 2024 21:44:09 +0100 Subject: [PATCH] v0.2 --- .../forms/Connectors/Bring/index.ts | 1 + .../forms/Connectors/Bring/postalCode.ts | 67 +++++++ .../forms/Connectors/createContext.ts | 57 ++++++ .../src/extensions/forms/Connectors/index.ts | 2 + .../PostalCodeAndCity/PostalCodeAndCity.tsx | 15 +- .../stories/PostalCodeAndCity.stories.tsx | 182 +++++------------- .../extensions/forms/hooks/useConnector.ts | 101 ---------- .../dnb-eufemia/src/extensions/forms/index.ts | 1 + .../dnb-eufemia/src/extensions/forms/types.ts | 7 - 9 files changed, 174 insertions(+), 259 deletions(-) create mode 100644 packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/index.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/postalCode.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Connectors/createContext.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Connectors/index.ts delete mode 100644 packages/dnb-eufemia/src/extensions/forms/hooks/useConnector.ts diff --git a/packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/index.ts b/packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/index.ts new file mode 100644 index 00000000000..5e40c737aca --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/index.ts @@ -0,0 +1 @@ +export * as postalCode from './postalCode' diff --git a/packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/postalCode.ts b/packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/postalCode.ts new file mode 100644 index 00000000000..ffc41bc37ab --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Connectors/Bring/postalCode.ts @@ -0,0 +1,67 @@ +import setData from '../../Form/data-context/setData' +import { GeneralConfig, fetchDataFromAPI } from '../' +import { UseFieldProps } from '../../types' + +export type HandlerConfig = { + cityPath: string +} + +async function fetchData(value: string, generalConfig: GeneralConfig) { + // Mock API response + // await new Promise((resolve) => setTimeout(resolve, 800)) + // 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 + generalConfig.fetchConfig.url = `https://cors-anywhere.herokuapp.com/https://api.bring.com/address/api/no/postal-codes/${value}` + + return await fetchDataFromAPI(generalConfig) +} + +export function onChange( + generalConfig: GeneralConfig, + handlerConfig?: HandlerConfig +): UseFieldProps['onChange'] { + return async function onChange(value) { + if (typeof value === 'string' && value.length >= 4) { + try { + const data = await fetchData(value, generalConfig) + // console.log('onChange', generalConfig, handlerConfig, value, data) + + const { city } = data.postal_codes[0] || {} + if (handlerConfig?.cityPath) { + const dataContext = setData(generalConfig.handlerId) + dataContext.update(handlerConfig.cityPath, city) + } + } catch (error) { + return new Error(error) + } + } + } +} + +export function onBlurValidator( + generalConfig: GeneralConfig +): UseFieldProps['onBlurValidator'] { + return async function onBlurValidator(value) { + try { + const data = await fetchData(value, generalConfig) + // console.log('onBlurValidator', generalConfig, value, data) + + if (data.postal_codes?.[0]?.postal_code !== value) { + return new Error('💁‍♂️ Feil i postnummeret') + } + } catch (error) { + return new Error(error) + } + } +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Connectors/createContext.ts b/packages/dnb-eufemia/src/extensions/forms/Connectors/createContext.ts new file mode 100644 index 00000000000..e04cf3c1020 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Connectors/createContext.ts @@ -0,0 +1,57 @@ +import { SharedStateId } from '../../../shared/helpers/useSharedState' + +export type GeneralConfig = { + handlerId?: SharedStateId + fetchConfig?: { + url?: string + headers?: Record + } +} + +export function createContext( + generalConfig: GeneralConfigGeneric = null +) { + const handlerId = {} + if (!generalConfig['handlerId']) { + generalConfig['handlerId'] = handlerId + } + + return { + handlerId, + withConfig< + HandlerMethod extends ( + generalConfig: GeneralConfigGeneric, + handlerConfig: unknown + ) => ReturnType, + >(fn: HandlerMethod, handlerConfig?: Parameters[1]) { + return fn(generalConfig, handlerConfig) + }, + } +} + +export async function fetchDataFromAPI(generalConfig: GeneralConfig) { + const { fetchConfig } = generalConfig + + const options = { + method: 'GET', + headers: { + Accept: 'application/json', + ...fetchConfig.headers, + }, + } + + try { + const response = await fetch(fetchConfig.url, options) + + // Check if the response status is in the range of 200-299 + if (!response.ok) { + throw new Error( + `HTTP Error: ${response.status} - ${response.statusText}` + ) + } + + return await response.json() + } catch (error) { + throw new Error(error) + } +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Connectors/index.ts b/packages/dnb-eufemia/src/extensions/forms/Connectors/index.ts new file mode 100644 index 00000000000..536d2b822fb --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Connectors/index.ts @@ -0,0 +1,2 @@ +export * from './createContext' +export * as Bring from './Bring' diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx index 0c675b5e692..30acb32685e 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx @@ -3,10 +3,9 @@ import classnames from 'classnames' import { Props as FieldBlockProps } from '../../FieldBlock' import StringField, { Props as StringFieldProps } from '../String' import CompositionField from '../Composition' -import { ConnectorProps, Path } from '../../types' +import { 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' @@ -23,7 +22,6 @@ export type Props = Pick< */ country?: Path | string help?: HelpProps - connector?: Record<'postalCode' | 'city', ConnectorProps> } function PostalCodeAndCity(props: Props) { @@ -36,16 +34,9 @@ function PostalCodeAndCity(props: Props) { help, width = 'large', country = defaultCountry, - connector, ...fieldBlockProps } = props - const postalCodeConnector = useConnector( - connector?.postalCode, - props - ) - const cityConnector = useConnector(connector?.city, props) - const { pattern: cityPattern, className: cityClassName, @@ -107,8 +98,6 @@ function PostalCodeAndCity(props: Props) { mask={postalCodeMask} pattern={postalCodePattern} placeholder={postalCodePlaceHolder} - onChange={postalCodeConnector?.onChange} - onBlurValidator={postalCodeConnector?.onBlurValidator} errorMessages={useMemo( () => ({ 'Field.errorRequired': translations.PostalCode.errorRequired, @@ -135,8 +124,6 @@ function PostalCodeAndCity(props: Props) { cityClassName )} label={cityLabel ?? translations.City.label} - onChange={cityConnector?.onChange} - onBlurValidator={cityConnector?.onBlurValidator} errorMessages={useMemo( () => ({ 'Field.errorRequired': translations.City.errorRequired, diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/stories/PostalCodeAndCity.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/stories/PostalCodeAndCity.stories.tsx index d8445a89de0..07ed5336aa4 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/stories/PostalCodeAndCity.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/stories/PostalCodeAndCity.stories.tsx @@ -1,5 +1,4 @@ -import { Field, Form, Value } from '../../..' -import { Flex } from '../../../../../components' +import { Connectors, Field, Form, Value } from '../../..' export default { title: 'Eufemia/Extensions/Forms/PostalCodeAndCity', @@ -52,146 +51,55 @@ 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)) - // }, - }, - } +// 1. Create a context with the config +const { withConfig, handlerId } = Connectors.createContext({ + fetchConfig: { + headers: { + 'X-Mybring-API-Uid': '', + 'X-Mybring-API-Key': '', }, }, -} +}) + +// 2. Use the context to create the onBlurValidator and onChange functions +const onBlurValidator = withConfig( + Connectors.Bring.postalCode.onBlurValidator +) + +// Should we name "onChange" to "autocompleteOnChange" or something like that? +const onChange = withConfig(Connectors.Bring.postalCode.onChange, { + cityPath: '/city', // or targetPath? +}) export function PostalCodeAPI_Draft() { return ( - - {/* { - // 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' }} - /> */} - - console.log('postalCode onChange', value), - }} - city={{ - placeholder: 'Your city', - onChange: (value) => console.log('city onChange', value), - }} - /> + { + await new Promise((resolve) => setTimeout(resolve, 3000)) + console.log('onSubmit', data) + }} + > + + { + // await new Promise((resolve) => setTimeout(resolve, 2000)) + // console.log('onChange', value) + // return { success: 'saved' } + // }, + }} + /> + + ) } - -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': '', -// // }, -// // }) -// // }, -// } diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useConnector.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useConnector.ts deleted file mode 100644 index 5229448f42c..00000000000 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useConnector.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { isAsync } from '../../../shared/helpers/isAsync' -import { ConnectorProps } from '../types' - -import { useCallback } from 'react' - -export default function useConnector( - connector: ConnectorProps = {}, - props: Record & ConnectorProps = {} -): { - onChange: ConnectorProps['onChange'] - onBlurValidator: ConnectorProps['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) { - return array.some((fn) => { - return isAsync(fn) - }) -} -function isFunctionInArray(array: Array) { - return array.some((fn) => { - return typeof fn === 'function' - }) -} diff --git a/packages/dnb-eufemia/src/extensions/forms/index.ts b/packages/dnb-eufemia/src/extensions/forms/index.ts index 2101e067d7c..ae208ee0d55 100644 --- a/packages/dnb-eufemia/src/extensions/forms/index.ts +++ b/packages/dnb-eufemia/src/extensions/forms/index.ts @@ -8,6 +8,7 @@ export * as Wizard from './Wizard' export * as DataContext from './DataContext' export * as Iterate from './Iterate' export * as Tools from './Tools' +export * as Connectors from './Connectors' export { default as FieldBlock } from './FieldBlock' export { default as ValueBlock } from './ValueBlock' export { default as Ajv } from 'ajv/dist/2020' diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index 34702fdc12e..89bbcf96a57 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -265,11 +265,6 @@ export type DataValueReadWriteComponentProps< DataValueReadProps & DataValueWriteProps -export type ConnectorProps = Pick< - UseFieldProps, - 'onChange' | 'onBlurValidator' -> - export interface UseFieldProps< Value = unknown, EmptyValue = undefined | unknown, @@ -405,8 +400,6 @@ export interface UseFieldProps< * For internal use only. */ valueType?: string | number | boolean | Array - - connector?: ConnectorProps } export type FieldProps<