Skip to content

Commit f2ccd5d

Browse files
tujoworkerlangz
andauthored
feat(Forms): add Bring API connector for postal code validation and autofill 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 <anderslangseth@gmail.com>
1 parent 8d0d980 commit f2ccd5d

File tree

22 files changed

+1734
-6
lines changed

22 files changed

+1734
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: 'Connectors'
3+
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.'
4+
showTabs: true
5+
order: 21
6+
tabs:
7+
- title: Info
8+
key: '/info'
9+
breadcrumb:
10+
- text: Forms
11+
href: /uilib/extensions/forms/
12+
- text: Connectors
13+
href: /uilib/extensions/forms/Connectors/
14+
accordion: true
15+
---
16+
17+
import Info from 'Docs/uilib/extensions/forms/Connectors/info'
18+
19+
<Info />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: 'Bring'
3+
description: 'Bring is a connector that allows you to fetch data from their REST API and use it in your form.'
4+
showTabs: true
5+
hideInMenu: true
6+
tabs:
7+
- title: Info
8+
key: '/info'
9+
- title: Demos
10+
key: '/demos'
11+
breadcrumb:
12+
- text: Forms
13+
href: /uilib/extensions/forms/
14+
- text: Connectors
15+
href: /uilib/extensions/forms/Connectors/
16+
- text: Bring
17+
href: /uilib/extensions/forms/Connectors/Bring/
18+
accordion: true
19+
---
20+
21+
import Info from 'Docs/uilib/extensions/forms/Connectors/Bring/info'
22+
import Demos from 'Docs/uilib/extensions/forms/Connectors/Bring/demos'
23+
24+
<Info />
25+
<Demos />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
2+
import { getMockData } from '@dnb/eufemia/src/extensions/forms/Connectors/Bring/postalCode'
3+
import { Form, Field, Connectors } from '@dnb/eufemia/src/extensions/forms'
4+
5+
let mockFetchTimeout = null
6+
async function mockFetch(countryCode: string) {
7+
const originalFetch = globalThis.fetch
8+
9+
globalThis.fetch = () => {
10+
return Promise.resolve({
11+
ok: true,
12+
json: () => {
13+
return Promise.resolve(getMockData(countryCode))
14+
},
15+
}) as any
16+
}
17+
18+
await new Promise((resolve) => setTimeout(resolve, 1000))
19+
20+
clearTimeout(mockFetchTimeout)
21+
mockFetchTimeout = setTimeout(() => {
22+
globalThis.fetch = originalFetch
23+
}, 1100)
24+
}
25+
26+
export const PostalCode = () => {
27+
return (
28+
<ComponentBox scope={{ Connectors, getMockData, mockFetch }}>
29+
{() => {
30+
const { withConfig } = Connectors.createContext({
31+
fetchConfig: {
32+
url: async (value, { countryCode }) => {
33+
await mockFetch(countryCode)
34+
return '[YOUR-API-URL]/' + value
35+
},
36+
},
37+
})
38+
39+
const onBlurValidator = withConfig(
40+
Connectors.Bring.postalCode.validator,
41+
)
42+
43+
const onChange = withConfig(Connectors.Bring.postalCode.autofill, {
44+
cityPath: '/city',
45+
})
46+
47+
return (
48+
<Form.Handler onSubmit={console.log}>
49+
<Form.Card>
50+
<Field.SelectCountry
51+
path="/countryCode"
52+
defaultValue="NO"
53+
filterCountries={({ iso }) => ['NO', 'SE'].includes(iso)}
54+
/>
55+
<Field.PostalCodeAndCity
56+
countryCode="/countryCode"
57+
postalCode={{
58+
path: '/postalCode',
59+
onBlurValidator,
60+
onChange,
61+
required: true,
62+
}}
63+
city={{
64+
path: '/city',
65+
required: true,
66+
}}
67+
/>
68+
</Form.Card>
69+
<Form.SubmitButton />
70+
</Form.Handler>
71+
)
72+
}}
73+
</ComponentBox>
74+
)
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
showTabs: true
3+
hideInMenu: true
4+
---
5+
6+
import * as Examples from './Examples'
7+
8+
## Demos
9+
10+
This demo contains only a mocked API call, so only a postal code of `1391` for Norway and `11432` for Sweden is valid.
11+
12+
<Examples.PostalCode />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
showTabs: true
3+
---
4+
5+
import { supportedCountryCodes } from '@dnb/eufemia/src/extensions/forms/Connectors/Bring/postalCode'
6+
7+
## Description
8+
9+
The `Bring` connector allows you to use the [Bring API](https://developer.bring.com/api/) to:
10+
11+
- Verify a postal code
12+
- Autofill a city name or street name
13+
14+
## PostalCode API
15+
16+
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.
17+
18+
First, create a context with the config:
19+
20+
```tsx
21+
import { Connectors, Field, Form } from '@dnb/eufemia/extensions/forms'
22+
23+
const { withConfig } = Connectors.createContext({
24+
fetchConfig: {
25+
url: (value, { countryCode }) => {
26+
return `[YOUR-API-URL]/.../${countryCode}/.../${value}`
27+
// Real-world example using Bring's Postal Code API's get postal code endpoint, directly without proxy:
28+
// return `https://api.bring.com/address/api/{countryCode}/postal-codes/{value}`
29+
},
30+
},
31+
})
32+
```
33+
34+
`[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.
35+
36+
### Supported countries
37+
38+
The Bring API for PostalCode supports the [following countries](https://developer.bring.com/api/postal-code/#supported-countries), by its country codes:
39+
40+
{supportedCountryCodes.join(', ')}
41+
42+
### Endpoints and response format
43+
44+
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:
45+
46+
```json
47+
{
48+
"postal_codes": [
49+
{
50+
"postal_code": "1391",
51+
"city": "Vollen"
52+
...
53+
}
54+
]
55+
}
56+
```
57+
58+
### To verify a postal code
59+
60+
Use the context to create a validator based on the `validator` connector.
61+
62+
You can use it for an `onChangeValidator` or `onBlurValidator` (recommended), depending on your use case.
63+
64+
```tsx
65+
const onBlurValidator = withConfig(Connectors.Bring.postalCode.validator)
66+
67+
function MyForm() {
68+
return (
69+
<Form.Handler>
70+
<Field.PostalCodeAndCity
71+
postalCode={{
72+
path: '/postalCode',
73+
onBlurValidator,
74+
}}
75+
/>
76+
</Form.Handler>
77+
)
78+
}
79+
```
80+
81+
### To autofill a city name based on a postal code
82+
83+
Use the context to create the `onChange` event handler based on the `autofill` connector.
84+
85+
```tsx
86+
const onChange = withConfig(Connectors.Bring.postalCode.autofill, {
87+
cityPath: '/city',
88+
})
89+
90+
function MyForm() {
91+
return (
92+
<Form.Handler>
93+
<Field.PostalCodeAndCity
94+
postalCode={{
95+
path: '/postalCode',
96+
onChange,
97+
}}
98+
city={{
99+
path: '/city',
100+
}}
101+
/>
102+
<Form.SubmitButton />
103+
</Form.Handler>
104+
)
105+
}
106+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
showTabs: true
3+
---
4+
5+
## Description
6+
7+
`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.
8+
9+
Available connectors:
10+
11+
- [Bring](/uilib/extensions/forms/Connectors/Bring/)
12+
13+
## Import
14+
15+
```ts
16+
import { Connectors } from '@dnb/eufemia/extensions/forms'
17+
```
18+
19+
## How to create your own connector
20+
21+
Connectors are created by returning a function that takes the `generalConfig` and optionally a `handlerConfig` as an argument.
22+
23+
Here is an example of how to create a connector that can be used as an field `onChangeValidator` or `onBlurValidator`:
24+
25+
```ts
26+
export function validator(generalConfig: GeneralConfig) {
27+
// - The handler to be used as the validator
28+
return async function validatorHandler(value) {
29+
try {
30+
const { data, status } = await fetchData(value, {
31+
generalConfig,
32+
parameters: {},
33+
})
34+
35+
const onMatch = () => {
36+
return new FormError('PostalCodeAndCity.invalidCode')
37+
}
38+
39+
const { matcher } = responseResolver(data, handlerConfig)
40+
const match = matcher(value)
41+
42+
if (status !== 400 && !match) {
43+
return onMatch()
44+
}
45+
} catch (error) {
46+
return error
47+
}
48+
}
49+
}
50+
```
51+
52+
Here is the `GeneralConfig` type simplified:
53+
54+
```ts
55+
type GeneralConfig = {
56+
fetchConfig?: {
57+
url: string | ((value: string) => string | Promise<string>)
58+
headers?: HeadersInit
59+
}
60+
}
61+
```
62+
63+
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.
64+
65+
```ts
66+
const responseResolver: ResponseResolver<
67+
PostalCodeResolverData,
68+
PostalCodeResolverPayload
69+
> = (data, handlerConfig) => {
70+
// - Here we align the data from the API with the expected data structure
71+
const { postal_code, city } = data?.postal_codes?.[0] || {}
72+
73+
return {
74+
/**
75+
* The matcher to be used to determine if the connector,
76+
* such as an validator for `onChangeValidator` or `onBlurValidator`,
77+
* should validate the field value.
78+
*/
79+
matcher: (value) => value === postal_code,
80+
81+
/**
82+
* The payload to be returned and used by the connector.
83+
*/
84+
payload: { city },
85+
}
86+
}
87+
```
88+
89+
You can extend a response resolver to support a custom resolver, given via the `handlerConfig` argument.
90+
91+
```ts
92+
const responseResolver = (data, handlerConfig) => {
93+
const resolver = handlerConfig?.responseResolver
94+
if (typeof resolver === 'function') {
95+
return resolver(data)
96+
}
97+
98+
// ... the rest of the response resolver.
99+
}
100+
```

packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/all-features.mdx

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ListIterateComponents from './Iterate/ListIterateComponents'
1919
**Table of Contents**
2020

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

9091
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)
9192

93+
## [Connectors](/uilib/extensions/forms/Connectors/)
94+
95+
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.
96+
9297
## [Wizard](/uilib/extensions/forms/Wizard/)
9398

9499
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.

packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/create-component/useFieldProps/info.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ const { error, hasError } = useFieldProps({
228228
handleChange(value, (additionalArgs = null))
229229
```
230230

231-
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.
231+
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`.
232232

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

packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/PostalCodeAndCity/info.mdx

+4
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ render(<Field.PostalCodeAndCity />)
1515
```
1616

1717
There is a corresponding [Value.PostalCodeAndCity](/uilib/extensions/forms/Value/PostalCodeAndCity) component.
18+
19+
## Validation and autofill
20+
21+
Read more about how to use the [Bring API](/uilib/extensions/forms/Connectors/Bring/) to validate and autofill a postal code and city name.

0 commit comments

Comments
 (0)