Skip to content

Commit

Permalink
feat(Forms): add Bring API connector for postal code validation and a…
Browse files Browse the repository at this point in the history
…utofill city (#4554)

- **Preview:** [Bring
connector](https://eufemia-git-feat-forms-api-connectors-bring-eufemia.vercel.app/uilib/extensions/forms/Connectors/Bring/)
- **Fully Treeshakeable:** Ensures no unused code is bundled.
- **Flexible and Extendable:** Designed to adapt to future needs.
- **Leverages Existing APIs:** Built on current field props and APIs.

### Example code:

```tsx
import { Form, Connectors } from '@dnb/eufemia/extensions/forms'

// 1. Create a context with the config
const { withConfig, handlerId } = Connectors.createContext({
  fetchConfig: {
    url: '...',
    headers: {
      'X-Mybring-API-Uid': '',
    },
  },
})

// 2. Use the context to create the onChangeValidator and onChange functions
const onChangeValidator = withConfig(Connectors.Bring.postalCode.validator)

// Should we name ".postalCode.onChange" to ".postalCode.autocompleteOnChange" or something like that?
const onChange = withConfig(Connectors.Bring.postalCode.autofill, {
  cityPath: '/city',
})

render(
  <Form.Handler
    id={handlerId}
  >
    <Field.PostalCodeAndCity
      postalCode={{
        path: '/postalCode',
        onChange,
        onChangeValidator,
      }}
      city={{
        path: '/city',
      }}
    />
  </Form.Handler>
)
```

---------

Co-authored-by: Anders <[email protected]>
  • Loading branch information
tujoworker and langz authored Feb 16, 2025
1 parent 8d0d980 commit f2ccd5d
Show file tree
Hide file tree
Showing 22 changed files with 1,734 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: 'Connectors'
description: 'Connectors are an opt-in way to extend the functionality of a form. They can be used to add features like API calls for autofill, validation, and more.'
showTabs: true
order: 21
tabs:
- title: Info
key: '/info'
breadcrumb:
- text: Forms
href: /uilib/extensions/forms/
- text: Connectors
href: /uilib/extensions/forms/Connectors/
accordion: true
---

import Info from 'Docs/uilib/extensions/forms/Connectors/info'

<Info />
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: 'Bring'
description: 'Bring is a connector that allows you to fetch data from their REST API and use it in your form.'
showTabs: true
hideInMenu: true
tabs:
- title: Info
key: '/info'
- title: Demos
key: '/demos'
breadcrumb:
- text: Forms
href: /uilib/extensions/forms/
- text: Connectors
href: /uilib/extensions/forms/Connectors/
- text: Bring
href: /uilib/extensions/forms/Connectors/Bring/
accordion: true
---

import Info from 'Docs/uilib/extensions/forms/Connectors/Bring/info'
import Demos from 'Docs/uilib/extensions/forms/Connectors/Bring/demos'

<Info />
<Demos />
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
import { getMockData } from '@dnb/eufemia/src/extensions/forms/Connectors/Bring/postalCode'
import { Form, Field, Connectors } from '@dnb/eufemia/src/extensions/forms'

let mockFetchTimeout = null
async function mockFetch(countryCode: string) {
const originalFetch = globalThis.fetch

globalThis.fetch = () => {
return Promise.resolve({
ok: true,
json: () => {
return Promise.resolve(getMockData(countryCode))
},
}) as any
}

await new Promise((resolve) => setTimeout(resolve, 1000))

clearTimeout(mockFetchTimeout)
mockFetchTimeout = setTimeout(() => {
globalThis.fetch = originalFetch
}, 1100)
}

export const PostalCode = () => {
return (
<ComponentBox scope={{ Connectors, getMockData, mockFetch }}>
{() => {
const { withConfig } = Connectors.createContext({
fetchConfig: {
url: async (value, { countryCode }) => {
await mockFetch(countryCode)
return '[YOUR-API-URL]/' + value
},
},
})

const onBlurValidator = withConfig(
Connectors.Bring.postalCode.validator,
)

const onChange = withConfig(Connectors.Bring.postalCode.autofill, {
cityPath: '/city',
})

return (
<Form.Handler onSubmit={console.log}>
<Form.Card>
<Field.SelectCountry
path="/countryCode"
defaultValue="NO"
filterCountries={({ iso }) => ['NO', 'SE'].includes(iso)}
/>
<Field.PostalCodeAndCity
countryCode="/countryCode"
postalCode={{
path: '/postalCode',
onBlurValidator,
onChange,
required: true,
}}
city={{
path: '/city',
required: true,
}}
/>
</Form.Card>
<Form.SubmitButton />
</Form.Handler>
)
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
showTabs: true
hideInMenu: true
---

import * as Examples from './Examples'

## Demos

This demo contains only a mocked API call, so only a postal code of `1391` for Norway and `11432` for Sweden is valid.

<Examples.PostalCode />
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
showTabs: true
---

import { supportedCountryCodes } from '@dnb/eufemia/src/extensions/forms/Connectors/Bring/postalCode'

## Description

The `Bring` connector allows you to use the [Bring API](https://developer.bring.com/api/) to:

- Verify a postal code
- Autofill a city name or street name

## PostalCode API

Here is an example of how to use the Bring [Postal Code API](https://developer.bring.com/api/postal-code/) to connect the [PostalCodeAndCity](/uilib/extensions/forms/feature-fields/PostalCodeAndCity/) field to a form.

First, create a context with the config:

```tsx
import { Connectors, Field, Form } from '@dnb/eufemia/extensions/forms'

const { withConfig } = Connectors.createContext({
fetchConfig: {
url: (value, { countryCode }) => {
return `[YOUR-API-URL]/.../${countryCode}/.../${value}`
// Real-world example using Bring's Postal Code API's get postal code endpoint, directly without proxy:
// return `https://api.bring.com/address/api/{countryCode}/postal-codes/{value}`
},
},
})
```

`[YOUR-API-URL]` is the URL of your own API endpoint that proxies the Bring [Postal Code API](https://developer.bring.com/api/postal-code/) with a token.

### Supported countries

The Bring API for PostalCode supports the [following countries](https://developer.bring.com/api/postal-code/#supported-countries), by its country codes:

{supportedCountryCodes.join(', ')}

### Endpoints and response format

Ensure you use one of the [following endpoints](https://developer.bring.com/api/postal-code/#endpoints) from Bring via your proxy API, returning a list of postal codes in the following format:

```json
{
"postal_codes": [
{
"postal_code": "1391",
"city": "Vollen"
...
}
]
}
```

### To verify a postal code

Use the context to create a validator based on the `validator` connector.

You can use it for an `onChangeValidator` or `onBlurValidator` (recommended), depending on your use case.

```tsx
const onBlurValidator = withConfig(Connectors.Bring.postalCode.validator)

function MyForm() {
return (
<Form.Handler>
<Field.PostalCodeAndCity
postalCode={{
path: '/postalCode',
onBlurValidator,
}}
/>
</Form.Handler>
)
}
```

### To autofill a city name based on a postal code

Use the context to create the `onChange` event handler based on the `autofill` connector.

```tsx
const onChange = withConfig(Connectors.Bring.postalCode.autofill, {
cityPath: '/city',
})

function MyForm() {
return (
<Form.Handler>
<Field.PostalCodeAndCity
postalCode={{
path: '/postalCode',
onChange,
}}
city={{
path: '/city',
}}
/>
<Form.SubmitButton />
</Form.Handler>
)
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
showTabs: true
---

## Description

`Connectors` are an opt-in way to extend the functionality of a form. They can be used to add features like API calls for autofill, validation, and more.

Available connectors:

- [Bring](/uilib/extensions/forms/Connectors/Bring/)

## Import

```ts
import { Connectors } from '@dnb/eufemia/extensions/forms'
```

## How to create your own connector

Connectors are created by returning a function that takes the `generalConfig` and optionally a `handlerConfig` as an argument.

Here is an example of how to create a connector that can be used as an field `onChangeValidator` or `onBlurValidator`:

```ts
export function validator(generalConfig: GeneralConfig) {
// - The handler to be used as the validator
return async function validatorHandler(value) {
try {
const { data, status } = await fetchData(value, {
generalConfig,
parameters: {},
})

const onMatch = () => {
return new FormError('PostalCodeAndCity.invalidCode')
}

const { matcher } = responseResolver(data, handlerConfig)
const match = matcher(value)

if (status !== 400 && !match) {
return onMatch()
}
} catch (error) {
return error
}
}
}
```

Here is the `GeneralConfig` type simplified:

```ts
type GeneralConfig = {
fetchConfig?: {
url: string | ((value: string) => string | Promise<string>)
headers?: HeadersInit
}
}
```
The `responseResolver` is used to take care of the response from the API and return the `matcher` and `payload` to be used by the connector.
```ts
const responseResolver: ResponseResolver<
PostalCodeResolverData,
PostalCodeResolverPayload
> = (data, handlerConfig) => {
// - Here we align the data from the API with the expected data structure
const { postal_code, city } = data?.postal_codes?.[0] || {}

return {
/**
* The matcher to be used to determine if the connector,
* such as an validator for `onChangeValidator` or `onBlurValidator`,
* should validate the field value.
*/
matcher: (value) => value === postal_code,

/**
* The payload to be returned and used by the connector.
*/
payload: { city },
}
}
```

You can extend a response resolver to support a custom resolver, given via the `handlerConfig` argument.

```ts
const responseResolver = (data, handlerConfig) => {
const resolver = handlerConfig?.responseResolver
if (typeof resolver === 'function') {
return resolver(data)
}

// ... the rest of the response resolver.
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ListIterateComponents from './Iterate/ListIterateComponents'
**Table of Contents**

- [Form](#form)
- [Connectors](#connectors)
- [Wizard](#wizard)
- [Iterate](#iterate)
- [Data Context](#data-context)
Expand Down Expand Up @@ -89,6 +90,10 @@ This is useful if you want to use a custom schema keyword and `validate` functio

You can also easily generate a Ajv schema from a set of fields (JSX), by using the `log` property on the `Tools.GenerateSchema` component. I will e.gc. console log the generated schema. More info about this feature can be found [on a separate page](/uilib/extensions/forms/Form/schema-validation/#generate-schema-from-fields)

## [Connectors](/uilib/extensions/forms/Connectors/)

Connectors are an opt-in way to extend the functionality of a form. They can be used to add features like API calls for autofill, validation, and more.

## [Wizard](/uilib/extensions/forms/Wizard/)

Wizard is a wrapper component for showing forms with a StepIndicator for navigation between several pages (multi-steps). It also includes components for navigating between steps.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const { error, hasError } = useFieldProps({
handleChange(value, (additionalArgs = null))
```

When `additionalArgs` is provided, it will be passed to the `onChange`, `onFocus` or `onBlur` events as the second argument. It will be merged with the internal `additionalArgs`, which includes `props`, including all of the given properties.
When `additionalArgs` is provided, it will be passed to the `onChange`, `onFocus` or `onBlur` events as the second argument. It will be merged with the internal `additionalArgs`, which includes `props` (including all of the given properties), `getValueByPath` and `getSourceValue`.

- `updateValue(value)` to update/change the internal value, without calling any events.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ render(<Field.PostalCodeAndCity />)
```

There is a corresponding [Value.PostalCodeAndCity](/uilib/extensions/forms/Value/PostalCodeAndCity) component.

## Validation and autofill

Read more about how to use the [Bring API](/uilib/extensions/forms/Connectors/Bring/) to validate and autofill a postal code and city name.
Loading

0 comments on commit f2ccd5d

Please sign in to comment.