diff --git a/README.md b/README.md
index ee145ca..56ee400 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 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
+
+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 } : {}),
};
};