From f226bee4cdaf345f5b9e62dd15db9fceb03a46d0 Mon Sep 17 00:00:00 2001 From: v1lling Date: Wed, 28 Jan 2026 13:58:28 +0100 Subject: [PATCH 1/2] Add onBlur callback support for form fields --- README.md | 14 +++++++++++++- src/assets/schemas/config.schema.json | 5 ++++- src/common/models/config.model.ts | 2 ++ src/components/formPopup/formPopup.comp.tsx | 10 ++++++++++ src/components/formRow/formRow.comp.tsx | 4 +++- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee145ca..d83262e 100644 --- a/README.md +++ b/README.md @@ -753,7 +753,19 @@ A list of fields you want us to send as the body of the request. Each one is an | multi | `boolean` | false | If true, select-multi dropdown will allow for multiple selections from a pre-defined list. Make sure defining the right input type first: `"type": "select-multi"`. | | selectLimit | `number` | unlimited | An optional setting for limiting the multiple selections from a pre-defined list. | | showFieldWhen | `function` | false | Only available when using dynamic (js) config.
A function that dynamically controls field visibility and inclusion in API requests. Only available when using dynamic (js) config. The function receives an array of all current form fields and must return a boolean value. When `true`, the field is displayed in the form and included in API requests. When `false`, the field is hidden and null value is sent with the API requests. Use the `originalName` property to reference other fields in your conditions.
Common use cases:
- Show additional fields based on a dropdown selection
- Show advanced options based on other field values
- Create dependent field relationships
Example:` "showFieldWhen": (fields) => { return fields.find(f => f.originalName === 'jobTitle')?.value === 'boss';}` | -| onChange | `function` | false | Only available when using dynamic (js) config.
A custom callback function that is triggered when the field's value changes. The function receives the new value of the changed field and the array of all current form fields. Use this to implement field dependencies, such as resetting another field when a field changes to a particular value.
Example:` "onChange": (newValue, fields) => { if (newValue === "B") { const dependentField = fields.find(f => f.originalName === "dependentField"); if (dependentField) { dependentField.value = null; } } }` | +| onChange | `function` | false | Only available when using dynamic (js) config.
A custom callback function that is triggered when the field's value changes. Fires on every keystroke for text inputs - for text fields, consider using `onBlur` instead. The function receives the new value and the array of all current form fields.
Example:` "onChange": (newValue, fields) => { if (newValue === "B") { const dependentField = fields.find(f => f.originalName === "dependentField"); if (dependentField) { dependentField.value = null; } } }` | +| onBlur | `function` | false | Only available when using dynamic (js) config.
A custom callback function that is triggered when the field loses focus (after editing completes). The function receives the current value of the field and the array of all current form fields. Useful for actions that should only run after the user finishes editing, like fetching dependent data based on completed input.
Example:` "onBlur": (value, fields) => { if (value) { const portField = fields.find(f => f.originalName === "port"); if (portField) { portField.value = 8080; } } }` | + +#### Field Callbacks: onChange vs onBlur + +Both callbacks receive the field value and all form fields, but fire at different times: + +| Callback | Fires when | Best for | +|----------|-----------|----------| +| `onChange` | Every change (each keystroke for text inputs) | Checkboxes, dropdowns, toggles - inputs where each change is a deliberate action | +| `onBlur` | Field loses focus (user clicks away or tabs out) | Text, number, email inputs - when you want to react after user finishes typing | + +**Recommendation**: Use `onBlur` for text-based inputs to avoid running your callback on every keystroke. Use `onChange` for checkboxes and select dropdowns where each interaction is intentional.
diff --git a/src/assets/schemas/config.schema.json b/src/assets/schemas/config.schema.json index bd5945e..8d1aae9 100644 --- a/src/assets/schemas/config.schema.json +++ b/src/assets/schemas/config.schema.json @@ -439,7 +439,10 @@ "description": "A function that determines if the field should be visible. The function receives an array of all form fields as its argument and should return a boolean. Example: `(fields) => fields.find(f => f.originalName === 'status')?.value === 'active'`" }, "onChange": { - "description": "A callback function that is triggered when the field's value changes. The function receives the new value and the array of all form fields as arguments. Example: `(value, fields) => { if (value === 'B') { const dependentField = fields.find(f => f.originalName === 'dependentField'); if (dependentField) { dependentField.value = null; } } }`" + "description": "A callback function that is triggered when the field's value changes. The function receives the new value and the array of all form fields as arguments. Best for checkboxes, dropdowns, and other inputs where each change is a deliberate action. Example: `(value, fields) => { if (value === 'B') { const dependentField = fields.find(f => f.originalName === 'dependentField'); if (dependentField) { dependentField.value = null; } } }`" + }, + "onBlur": { + "description": "A callback function that is triggered when the field loses focus (after editing completes). The function receives the current value and the array of all form fields as arguments. Best for text, number, email inputs - when you want to react after the user finishes typing rather than on every keystroke. Example: `(value, fields) => { if (value) { const dependentField = fields.find(f => f.originalName === 'dependentField'); if (dependentField) { dependentField.value = 'default'; } } }`" }, "label": { "type": "string", diff --git a/src/common/models/config.model.ts b/src/common/models/config.model.ts index 3dac9e3..cd4d73d 100644 --- a/src/common/models/config.model.ts +++ b/src/common/models/config.model.ts @@ -159,6 +159,7 @@ export type TConfigInputField = type showFieldWhen = (fields: IConfigInputField[]) => boolean; type onChangeCallback = (newValue: any, fields: IConfigInputField[]) => void; +type onBlurCallback = (value: any, fields: IConfigInputField[]) => void; export interface IConfigInputField { originalName?: string; @@ -180,6 +181,7 @@ export interface IConfigInputField { selectLimit?: number; showFieldWhen?: showFieldWhen; onChange?: onChangeCallback; + onBlur?: onBlurCallback; } export interface IConfigOptionSource { diff --git a/src/components/formPopup/formPopup.comp.tsx b/src/components/formPopup/formPopup.comp.tsx index 66509f8..ee7aaeb 100644 --- a/src/components/formPopup/formPopup.comp.tsx +++ b/src/components/formPopup/formPopup.comp.tsx @@ -263,6 +263,15 @@ export const FormPopup = withAppContext(({ context, title, type, successMessage, setFormFields(updatedFormFields); } + function fieldBlurred(fieldName: string) { + const field = formFields.find(f => f.name === fieldName); + if (field?.onBlur) { + field.onBlur(field.value, formFields); + // Trigger re-render to reflect any changes made by the callback + setFormFields([...formFields]); + } + } + // Check if field should be visible based on showFieldWhen condition function shouldFieldBeVisible(field: IConfigInputField, fields: IConfigInputField[]): boolean { if (!field.showFieldWhen) return true; @@ -312,6 +321,7 @@ export const FormPopup = withAppContext(({ context, title, type, successMessage, key={`field_${idx}`} field={field} onChange={formChanged} + onBlur={fieldBlurred} showReset={!field.type || field.type === 'text'} /> ); diff --git a/src/components/formRow/formRow.comp.tsx b/src/components/formRow/formRow.comp.tsx index c61922c..c31b71f 100644 --- a/src/components/formRow/formRow.comp.tsx +++ b/src/components/formRow/formRow.comp.tsx @@ -27,6 +27,7 @@ interface IProps { value: any, submitAfterChange?: boolean ) => void; + onBlur?: (fieldName: string) => void; showReset?: boolean; direction?: "row" | "column"; } @@ -38,7 +39,7 @@ interface IOption { } export const FormRow = withAppContext( - ({ context, field, direction, showReset, onChange }: IProps) => { + ({ context, field, direction, showReset, onChange, onBlur }: IProps) => { const [optionSources, setOptionSources] = useState({}); const [originalOptions, setOriginalOptions] = useState({}); const { httpService, activePage, config } = context; @@ -229,6 +230,7 @@ export const FormRow = withAppContext( disabled: field.readonly, required: field.required, onChange: (e: any) => changeCallback(field.name, e.target.value), + onBlur: () => onBlur?.(field.name), ...(helpText ? { "aria-describedby": helpTextId } : {}), }; }; From b32aebdf66714277ad4e0ecf5165fb26103f2c07 Mon Sep 17 00:00:00 2001 From: v1lling Date: Wed, 28 Jan 2026 13:58:59 +0100 Subject: [PATCH 2/2] Update onBlur callback example to set default values after editing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d83262e..56ee400 100644 --- a/README.md +++ b/README.md @@ -754,7 +754,7 @@ A list of fields you want us to send as the body of the request. Each one is an | selectLimit | `number` | unlimited | An optional setting for limiting the multiple selections from a pre-defined list. | | showFieldWhen | `function` | false | Only available when using dynamic (js) config.
A function that dynamically controls field visibility and inclusion in API requests. Only available when using dynamic (js) config. The function receives an array of all current form fields and must return a boolean value. When `true`, the field is displayed in the form and included in API requests. When `false`, the field is hidden and null value is sent with the API requests. Use the `originalName` property to reference other fields in your conditions.
Common use cases:
- Show additional fields based on a dropdown selection
- Show advanced options based on other field values
- Create dependent field relationships
Example:` "showFieldWhen": (fields) => { return fields.find(f => f.originalName === 'jobTitle')?.value === 'boss';}` | | onChange | `function` | false | Only available when using dynamic (js) config.
A custom callback function that is triggered when the field's value changes. Fires on every keystroke for text inputs - for text fields, consider using `onBlur` instead. The function receives the new value and the array of all current form fields.
Example:` "onChange": (newValue, fields) => { if (newValue === "B") { const dependentField = fields.find(f => f.originalName === "dependentField"); if (dependentField) { dependentField.value = null; } } }` | -| onBlur | `function` | false | Only available when using dynamic (js) config.
A custom callback function that is triggered when the field loses focus (after editing completes). The function receives the current value of the field and the array of all current form fields. Useful for actions that should only run after the user finishes editing, like fetching dependent data based on completed input.
Example:` "onBlur": (value, fields) => { if (value) { const portField = fields.find(f => f.originalName === "port"); if (portField) { portField.value = 8080; } } }` | +| onBlur | `function` | false | Only available when using dynamic (js) config.
A custom callback function that is triggered when the field loses focus (after editing completes). The function receives the current value of the field and the array of all current form fields. Useful for actions that should only run after the user finishes editing, like setting defaults based on completed input.
Example:` "onBlur": (value, fields) => { if (value) { const dependentField = fields.find(f => f.originalName === "dependentField"); if (dependentField) { dependentField.value = "default"; } } }` | #### Field Callbacks: onChange vs onBlur