Skip to content

Commit

Permalink
[open-formulieren/open-forms#5006] Updated addressNL component to man…
Browse files Browse the repository at this point in the history
…ually fill in city and streetname

Until now the addressNL component would retrieve the city and street
name based on the postcode and housenumber. These fields were always
read-only but now we want to be able to fill in city and street name if
something goes wrong with the API call.
  • Loading branch information
vaszig committed Jan 29, 2025
1 parent 263df05 commit 93b9e8b
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 122 deletions.
33 changes: 33 additions & 0 deletions src/components/FormStep/FormStep.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,36 @@ export const SummaryProgressNotVisible = {
expect(canvas.queryByText(/Stap 1 van 1/)).toBeNull();
},
};

export const AddressNLManuallyTriggeredValidation = {
render,
args: {
formioConfiguration: {
display: 'form',
components: [
{
key: 'addressnl',
type: 'addressNL',
label: 'Address NL',
validate: {
required: false,
},
},
],
},
form: buildForm(),
submission: buildSubmission(),
},
play: async ({canvasElement}) => {
const canvas = within(canvasElement);

const postcodeInput = await canvas.findByLabelText('Postcode');
const submitButton = await canvas.findByRole('button', {name: 'Next'});

await userEvent.type(postcodeInput, '1017 CJ');

// wait for the check logic api call
await sleep(1800);
await userEvent.click(submitButton);
},
};
119 changes: 84 additions & 35 deletions src/formio/components/AddressNL.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import {Formik, useFormikContext} from 'formik';
import debounce from 'lodash/debounce';
import {useContext, useEffect} from 'react';
import {createRef, useContext, useEffect, useState} from 'react';
import {createRoot} from 'react-dom/client';
import {Formio} from 'react-formio';
import {FormattedMessage, IntlProvider, defineMessages, useIntl} from 'react-intl';
Expand All @@ -26,6 +26,8 @@ export default class AddressNL extends Field {
// the edit grid renderRow otherwise wraps the result of getValueAsString in a
// readonly input...
this.component.template = 'hack';
// needed for manually triggering the formik validate method
this.formikInnerRef = createRef();
}

static schema(...extend) {
Expand Down Expand Up @@ -56,11 +58,37 @@ export default class AddressNL extends Field {
};
}

checkComponentValidity(data, dirty, row, options = {}) {
async checkComponentValidity(data, dirty, row, options = {}) {
let updatedOptions = {...options};
if (this.component.validate.plugins && this.component.validate.plugins.length) {
updatedOptions.async = true;
}

if (!dirty) {
return super.checkComponentValidity(data, dirty, row, updatedOptions);
}

// Trigger again formik validation in order to show the generic error along with the
// nested fields errors and prevent the form from being submitted.
// Tried to go deeper for this in formio but this will be properly handled in the new
// form renderer.
if (this.formikInnerRef.current) {
const errors = await this.formikInnerRef.current.validateForm();

Check warning on line 76 in src/formio/components/AddressNL.jsx

View check run for this annotation

Codecov / codecov/patch

src/formio/components/AddressNL.jsx#L76

Added line #L76 was not covered by tests

if (Object.keys(errors).length > 0) {
this.setComponentValidity(

Check warning on line 79 in src/formio/components/AddressNL.jsx

View check run for this annotation

Codecov / codecov/patch

src/formio/components/AddressNL.jsx#L79

Added line #L79 was not covered by tests
[
{
message: this.t('There are errors concerning the nested fields.'),
level: 'error',
},
],
true,
false
);
return false;

Check warning on line 89 in src/formio/components/AddressNL.jsx

View check run for this annotation

Codecov / codecov/patch

src/formio/components/AddressNL.jsx#L89

Added line #L89 was not covered by tests
}
}
return super.checkComponentValidity(data, dirty, row, updatedOptions);
}

Expand All @@ -77,6 +105,7 @@ export default class AddressNL extends Field {
city: '',
streetName: '',
secretStreetCity: '',
autoPopulated: false,
};
}

Expand Down Expand Up @@ -169,6 +198,7 @@ export default class AddressNL extends Field {
deriveAddress={this.component.deriveAddress}
layout={this.component.layout}
setFormioValues={this.onFormikChange.bind(this)}
formikInnerRef={this.formikInnerRef}
/>
</ConfigContext.Provider>
</IntlProvider>
Expand Down Expand Up @@ -204,9 +234,25 @@ const FIELD_LABELS = defineMessages({
description: 'Label for addressNL houseNumber input',
defaultMessage: 'House number',
},
houseLetter: {
description: 'Label for addressNL houseLetter input',
defaultMessage: 'House letter',
},
houseNumberAddition: {
description: 'Label for addressNL houseNumberAddition input',
defaultMessage: 'House number addition',
},
streetName: {
description: 'Label for addressNL streetName input',
defaultMessage: 'Street name',
},
city: {
description: 'Label for addressNL city input',
defaultMessage: 'City',
},
});

const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => {
const addressNLSchema = (required, deriveAddress, intl, {postcode = {}, city = {}}) => {
// Optionally use a user-supplied pattern/regex for more fine grained pattern
// validation, and if a custom error message was supplied, use it.
const postcodeRegex = postcode?.pattern
Expand All @@ -221,6 +267,7 @@ const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => {
});
let postcodeSchema = z.string().regex(postcodeRegex, {message: postcodeErrorMessage});

let streetNameSchema = z.string();
const {pattern: cityPattern = '', errorMessage: cityErrorMessage = ''} = city;
let citySchema = z.string();
if (cityPattern) {
Expand All @@ -237,15 +284,22 @@ const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => {
defaultMessage: 'House number must be a number with up to five digits (e.g. 456).',
}),
});

if (!required) {
postcodeSchema = postcodeSchema.optional();
houseNumberSchema = houseNumberSchema.optional();
streetNameSchema = streetNameSchema.optional();
citySchema = citySchema.optional();

Check warning on line 292 in src/formio/components/AddressNL.jsx

View check run for this annotation

Codecov / codecov/patch

src/formio/components/AddressNL.jsx#L291-L292

Added lines #L291 - L292 were not covered by tests
} else if (!deriveAddress) {
streetNameSchema = streetNameSchema.optional();
citySchema = citySchema.optional();
}

return z
.object({
postcode: postcodeSchema,
city: citySchema.optional(),
streetName: streetNameSchema,
city: citySchema,
houseNumber: houseNumberSchema,
houseLetter: z
.string()
Expand Down Expand Up @@ -297,7 +351,14 @@ const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => {
});
};

const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormioValues}) => {
const AddressNLForm = ({
initialValues,
required,
deriveAddress,
layout,
setFormioValues,
formikInnerRef,
}) => {
const intl = useIntl();

const {
Expand Down Expand Up @@ -340,14 +401,16 @@ const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormi

return (
<Formik
innerRef={formikInnerRef}
initialValues={initialValues}
initialTouched={{
postcode: true,
houseNumber: true,
city: true,
streetName: true,
}}
validationSchema={toFormikValidationSchema(
addressNLSchema(required, intl, {
addressNLSchema(required, deriveAddress, intl, {
postcode: {
pattern: postcodePattern,
errorMessage: postcodeError,
Expand All @@ -373,6 +436,7 @@ const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormi
const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => {
const {values, isValid, setFieldValue} = useFormikContext();
const {baseUrl} = useContext(ConfigContext);
const [isAddressAutoFilled, setAddressAutoFilled] = useState(true);
const useColumns = layout === 'doubleColumn';

useEffect(() => {
Expand All @@ -399,6 +463,12 @@ const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => {
setFieldValue('city', data['city']);
setFieldValue('streetName', data['streetName']);
setFieldValue('secretStreetCity', data['secretStreetCity']);

// mark the auto-filled fields as populated and disabled when they have been both
// retrieved from the API and they do have a value
const dataRetrieved = !!(data['city'] && data['streetName']);
setAddressAutoFilled(dataRetrieved);
setFieldValue('autoPopulated', dataRetrieved);

Check warning on line 471 in src/formio/components/AddressNL.jsx

View check run for this annotation

Codecov / codecov/patch

src/formio/components/AddressNL.jsx#L470-L471

Added lines #L470 - L471 were not covered by tests
};

return (
Expand All @@ -411,45 +481,24 @@ const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => {
>
<PostCodeField required={required} autoFillAddress={autofillAddress} />
<HouseNumberField required={required} autoFillAddress={autofillAddress} />
<TextField
name="houseLetter"
label={
<FormattedMessage
description="Label for addressNL houseLetter input"
defaultMessage="House letter"
/>
}
/>
<TextField name="houseLetter" label={<FormattedMessage {...FIELD_LABELS.houseLetter} />} />
<TextField
name="houseNumberAddition"
label={
<FormattedMessage
description="Label for addressNL houseNumberAddition input"
defaultMessage="House number addition"
/>
}
label={<FormattedMessage {...FIELD_LABELS.houseNumberAddition} />}
/>
{deriveAddress && (
<>
<TextField
name="streetName"
label={
<FormattedMessage
description="Label for addressNL streetName read only result"
defaultMessage="Street name"
/>
}
disabled
label={<FormattedMessage {...FIELD_LABELS.streetName} />}
disabled={isAddressAutoFilled}
isRequired={required}
/>
<TextField
name="city"
label={
<FormattedMessage
description="Label for addressNL city read only result"
defaultMessage="City"
/>
}
disabled
label={<FormattedMessage {...FIELD_LABELS.city} />}
disabled={isAddressAutoFilled}
isRequired={required}
/>
</>
)}
Expand Down
55 changes: 55 additions & 0 deletions src/formio/components/AddressNL.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {screen} from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import {renderForm} from 'jstests/formio/utils';

const addressNLForm = {
type: 'form',
components: [
{
key: 'addressnl',
type: 'addressNL',
label: 'Address NL',
validate: {
required: true,
},
},
],
};

describe('The addressNL component', () => {
afterEach(() => {
document.body.innerHTML = '';
});
test('Postcode provided and missing housenumber', async () => {
const user = userEvent.setup({delay: 50});
await renderForm(addressNLForm, {
evalContext: {
requiredFieldsWithAsterisk: true,
},
});
const postcode = screen.getByLabelText('Postcode');
const houseNumber = screen.getByLabelText('House number');

await user.type(postcode, '1017 CJ');

expect(houseNumber).toHaveClass('utrecht-textbox--invalid');
expect(houseNumber).toHaveAttribute('aria-describedby');
expect(houseNumber).toHaveAttribute('aria-invalid');
});
test('Postcode missing and housenumber provided', async () => {
const user = userEvent.setup({delay: 50});
await renderForm(addressNLForm, {
evalContext: {
requiredFieldsWithAsterisk: true,
},
});
const postcode = screen.getByLabelText('Postcode');
const houseNumber = screen.getByLabelText('House number');

await user.type(houseNumber, '22');

expect(postcode).toHaveClass('utrecht-textbox--invalid');
expect(postcode).toHaveAttribute('aria-describedby');
expect(postcode).toHaveAttribute('aria-invalid');
});
});
Loading

0 comments on commit 93b9e8b

Please sign in to comment.