From 74147f9271b22cc9971154935846f31021fad1d3 Mon Sep 17 00:00:00 2001 From: robinvandermolen Date: Wed, 29 Jan 2025 16:26:40 +0100 Subject: [PATCH 1/2] :sparkles: [open-formulieren/open-forms#4699] Trigger AddressNL validation after blur --- src/formio/components/AddressNL.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/formio/components/AddressNL.jsx b/src/formio/components/AddressNL.jsx index c92fb0b5e..23789390b 100644 --- a/src/formio/components/AddressNL.jsx +++ b/src/formio/components/AddressNL.jsx @@ -346,6 +346,7 @@ const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormi houseNumber: true, city: true, }} + validateOnChange={false} validationSchema={toFormikValidationSchema( addressNLSchema(required, intl, { postcode: { From b16552c0511e8d128196c8c6b897aab43f72ab36 Mon Sep 17 00:00:00 2001 From: robinvandermolen Date: Wed, 29 Jan 2025 17:12:03 +0100 Subject: [PATCH 2/2] :sparkles: [open-formulieren/open-forms#4699] Trigger AddressNL validation after dirty To trigger the validation once the addressNL component has changed, we have to monitor formik changes. Using the `dirty` property from the `useFormikContext` we can simplify this monitoring. Unfortunately we can only set/alter the validation on the top level. With this we have three options: - We can add `onBlur` properties to all addressNL fields. This means that we can handle formValidation after blur, and only when `dirty === true`. This would add a lot of custom properties and logic, for a small 'problem'. This would also move us futher from a "the component functions on its own" way of working. - We can add `onBlur` eventListeners via `useEffect`. This would give us the same functionality as the previous option, without all the custom properties. It would also make the addressNL component even more complex and introduce unexpected behavior. - Finally we can change the validation rule based on the `dirty` property. For this to work, we would have to pass the `dirty` prop to the parent component, to then change the validation rule. It is a strange way to work, but would mean the least amount of custom behavior and keep the validation process understandable. For simplicity sake, I've chosen the last option. To use the `dirty` prop from the formik context to change the addressNL component validation --- src/formio/components/AddressNL.jsx | 53 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/formio/components/AddressNL.jsx b/src/formio/components/AddressNL.jsx index 23789390b..96ccc3895 100644 --- a/src/formio/components/AddressNL.jsx +++ b/src/formio/components/AddressNL.jsx @@ -3,12 +3,12 @@ */ import {Formik, useFormikContext} from 'formik'; import debounce from 'lodash/debounce'; -import {useContext, useEffect} from 'react'; +import {useContext, useEffect, useState} from 'react'; import {createRoot} from 'react-dom/client'; import {Formio} from 'react-formio'; import {FormattedMessage, IntlProvider, defineMessages, useIntl} from 'react-intl'; import {z} from 'zod'; -import {toFormikValidationSchema} from 'zod-formik-adapter'; +import {toFormikValidate} from 'zod-formik-adapter'; import {ConfigContext} from 'Context'; import {get} from 'api'; @@ -299,6 +299,7 @@ const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => { const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormioValues}) => { const intl = useIntl(); + const [dirty, setDirty] = useState(false); const { component: { @@ -338,6 +339,13 @@ const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormi return {message: ctx.defaultError}; // use global schema as fallback }; + const handleFormikAddressDirtyChange = newDirtyState => { + // Only set the dirty state from `false` to `true`. Once it's dirty, it will remain dirty. + if (!dirty && newDirtyState) { + setDirty(newDirtyState); + } + }; + return ( + dirty + ? toFormikValidate( + addressNLSchema(required, intl, { + postcode: { + pattern: postcodePattern, + errorMessage: postcodeError, + }, + city: { + pattern: cityPattern, + errorMessage: cityError, + }, + }), + {errorMap} + )(values) + : {} + } > ); }; -const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => { - const {values, isValid, setFieldValue} = useFormikContext(); +const FormikAddress = ({required, setFormioValues, deriveAddress, layout, setDirty}) => { + const {values, isValid, setFieldValue, dirty} = useFormikContext(); const {baseUrl} = useContext(ConfigContext); const useColumns = layout === 'doubleColumn'; + useEffect(() => { + if (dirty) { + setDirty(dirty); + } + }, [dirty, setDirty]); + useEffect(() => { // *always* synchronize the state up, since: //