From 295868bec99f58035d8e9fd4291fab57c5363960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Mon, 4 Apr 2022 15:20:33 +0100 Subject: [PATCH 1/2] [Form lib] Add "updateFieldValues" to the form hook API Fixes https://github.com/elastic/kibana/issues/128458 --- .../components/fields/combobox_field.tsx | 5 +- .../static/forms/docs/core/form_hook.mdx | 19 + .../components/use_array.test.tsx | 8 +- .../hook_form_lib/components/use_array.ts | 46 +-- .../hook_form_lib/hooks/use_form.test.tsx | 359 +++++++++++++++++- .../forms/hook_form_lib/hooks/use_form.ts | 109 +++++- .../static/forms/hook_form_lib/lib/index.ts | 2 +- .../forms/hook_form_lib/lib/utils.test.ts | 26 +- .../static/forms/hook_form_lib/lib/utils.ts | 18 + .../static/forms/hook_form_lib/types.ts | 24 ++ 10 files changed, 562 insertions(+), 54 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 981fd7c0a773b..7f3e476b21251 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -42,7 +42,8 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest }: Pr const onCreateComboOption = (value: string) => { // Note: for now, all validations for a comboBox array item have to be synchronous - // If there is a need to support asynchronous validation, we'll work on it (and will need to update the logic). + // If there is a need to support asynchronous validation, we'll need to update this handler and + // make the "onCreateOption" handler async). const { isValid } = field.validate({ value, validationType: VALIDATION_TYPES.ARRAY_ITEM, @@ -84,7 +85,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest }: Pr placeholder={i18n.translate('esUi.forms.comboBoxField.placeHolderText', { defaultMessage: 'Type and then hit "ENTER"', })} - selectedOptions={(field.value as any[]).map((v) => ({ label: v }))} + selectedOptions={(field.value as string[]).map((v) => ({ label: v }))} onCreateOption={onCreateComboOption} onChange={onComboChange} onSearchChange={onSearchComboChange} diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/form_hook.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/form_hook.mdx index d66c0d867c275..46fac236123fd 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/core/form_hook.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/core/form_hook.mdx @@ -212,3 +212,22 @@ Sets field errors imperatively. ```js form.setFieldErrors('name', [{ message: 'There is an error in the field' }]); ``` + +### updateFieldValues() + +**Arguments:** `updatedFormData: Partial, options?: { runDeserializer?: boolean }` + +Update multiple field values at once. You don't need to provide all the form fields, **partial** update is supported. This method is mainly useful to update an array of object fields or to avoid multiple `form.setFieldValue()` calls. + +```js +// Update an array of object (e.g "myArray[0].foo", "myArray[0].baz"...) +form.updateFieldValues({ + myArray: [ + { foo: 'bar', baz: true }, + { foo2: 'bar2', baz: false } + ] +}); + +// or simply multiple fields at once +form.updateFieldValues({ foo: 'bar', baz: false }) +``` \ No newline at end of file diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx index dc8695190bdaf..8e9bdd35ffb7e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx @@ -77,11 +77,7 @@ describe('', () => { <> {items.map(({ id, path }) => { return ( - + ); })} @@ -102,7 +98,7 @@ describe('', () => { } = setup(); await act(async () => { - setInputValue('nameField__0', 'John'); + setInputValue('users[0]Name', 'John'); }); const formData = onFormData.mock.calls[onFormData.mock.calls.length - 1][0]; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts index 78379aa9fffbf..27a51467c5bda 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import uuid from 'uuid'; import { useEffect, useRef, useCallback, useMemo } from 'react'; import { FormHook, FieldConfig } from '../types'; @@ -37,6 +36,24 @@ export interface FormArrayField { form: FormHook; } +let uniqueId = 0; + +export const createArrayItem = (path: string, index: number, isNew = true): ArrayItem => ({ + id: uniqueId++, + path: `${path}[${index}]`, + isNew, +}); + +/** + * We create an internal field to represent the Array items. This field is not returned + * as part as the form data but is used internally to run validation on the array items + * and its value (an array of ArrayItem) is used to map to actual form fields. + * + * @param path The array path in the form data + * @returns The internal array field path + */ +export const getInternalArrayFieldPath = (path: string): string => `${path}__array__`; + /** * Use UseArray to dynamically add fields to your form. * @@ -60,41 +77,26 @@ export const UseArray = ({ children, }: Props) => { const isMounted = useRef(false); - const uniqueId = useRef(0); const form = useFormContext(); const { getFieldDefaultValue } = form; - const getNewItemAtIndex = useCallback( - (index: number): ArrayItem => ({ - id: uniqueId.current++, - path: `${path}[${index}]`, - isNew: true, - }), - [path] - ); - const fieldDefaultValue = useMemo(() => { const defaultValues = readDefaultValueOnForm ? getFieldDefaultValue(path) : undefined; if (defaultValues) { - return defaultValues.map((_, index) => ({ - id: uniqueId.current++, - path: `${path}[${index}]`, - isNew: false, - })); + return defaultValues.map((_, index) => createArrayItem(path, index, false)); } - return new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); - }, [path, initialNumberOfItems, readDefaultValueOnForm, getFieldDefaultValue, getNewItemAtIndex]); + return new Array(initialNumberOfItems).fill('').map((_, i) => createArrayItem(path, i)); + }, [path, initialNumberOfItems, readDefaultValueOnForm, getFieldDefaultValue]); // Create an internal hook field which behaves like any other form field except that it is not // outputed in the form data (when calling form.submit() or form.getFormData()) // This allow us to run custom validations (passed to the props) on the Array items - - const internalFieldPath = useMemo(() => `${path}__${uuid.v4()}`, [path]); + const internalFieldPath = useMemo(() => getInternalArrayFieldPath(path), [path]); const fieldConfigBase: FieldConfig & InternalFieldConfig = { defaultValue: fieldDefaultValue, @@ -132,9 +134,9 @@ export const UseArray = ({ const addItem = useCallback(() => { setValue((previousItems) => { const itemIndex = previousItems.length; - return [...previousItems, getNewItemAtIndex(itemIndex)]; + return [...previousItems, createArrayItem(path, itemIndex)]; }); - }, [setValue, getNewItemAtIndex]); + }, [setValue, path]); const removeItem = useCallback( (id: number) => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index afaaaaedef23e..9f57432b5d6a1 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -6,12 +6,13 @@ * Side Public License, v 1. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; import { emptyField } from '../../helpers/field_validators'; -import { Form, UseField } from '../components'; +import { ComboBoxField } from '../../components'; +import { Form, UseField, UseArray } from '../components'; import { FormSubmitHandler, OnUpdateHandler, @@ -274,7 +275,7 @@ describe('useForm() hook', () => { onFormData.mockReset(); }); - test('should set the default value of a field ', async () => { + test('should set the default value of a field ', () => { const defaultValue = { title: getRandomString(), subTitle: getRandomString(), @@ -285,6 +286,7 @@ describe('useForm() hook', () => { const TestComp = ({ onData }: { onData: OnUpdateHandler }) => { const { form } = useForm({ defaultValue }); + formHook = form; const { subscribe } = form; useEffect(() => subscribe(onData).unsubscribe, [subscribe, onData]); @@ -316,6 +318,40 @@ describe('useForm() hook', () => { name: defaultValue.user.name, }, }); + + expect(formHook?.__getFormDefaultValue()).toEqual({ + ...defaultValue, + subTitle: 'hasBeenOverridden', + }); + }); + + test('should be updated with the UseField "defaultValue" prop', () => { + const TestComp = () => { + const { form } = useForm({ defaultValue: { name: 'Mike' } }); + const [_, setDate] = useState(new Date()); + formHook = form; + + return ( +
+ {/* "John" should be set in the form defaultValue */} + + + + ); + }; + + const { find } = registerTestBed(TestComp, { memoryRouter: { wrapComponent: false } })(); + + expect(formHook?.__getFormDefaultValue()).toEqual({ name: 'John' }); + + // Make sure a re-render of the component does not re-update the defaultValue + act(() => { + find('forceUpdateBtn').simulate('click'); + }); + + expect(formHook?.__getFormDefaultValue()).toEqual({ name: 'John' }); }); }); @@ -653,4 +689,321 @@ describe('useForm() hook', () => { expect(errors).toEqual(['Field1 can not be empty', 'Field2 is invalid']); }); }); + + describe('form.updateFieldValues()', () => { + test('should update field values and discard unknwon fields provided', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + + + + ); + }; + + registerTestBed(TestComp)(); + + expect(formHook!.getFormData()).toEqual({ + field1: 'field1_defaultValue', + field2: { + a: 'field2_a_defaultValue', + b: 'field2_b_defaultValue', + }, + }); + + await act(async () => { + formHook!.updateFieldValues({ + field1: 'field1_updated', + field2: { + a: 'field2_a_updated', + b: 'field2_b_updated', + }, + unknownField: 'foo', + }); + }); + + expect(formHook!.getFormData()).toEqual({ + field1: 'field1_updated', + field2: { + a: 'field2_a_updated', + b: 'field2_b_updated', + }, + }); + }); + + test('should update an array of object fields', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + {({ items }) => ( + <> + {items.map(({ id, path, isNew }) => ( +
+ + +
+ ))} + + )} +
+
+ ); + }; + + registerTestBed(TestComp)(); + + if (formHook === null) { + throw new Error('Formhook has not been set.'); + } + + expect(formHook.getFormData()).toEqual({ + users: [ + { + name: 'John', + lastName: 'Snow', + }, + ], + }); + + const newFormData = { + users: [ + { + name: 'User1_name', + lastName: 'User1_lastName', + }, + { + name: 'User2_name', + lastName: 'User2_lastName', + }, + ], + }; + + await act(async () => { + formHook!.updateFieldValues(newFormData); + }); + + expect(formHook.getFormData()).toEqual(newFormData); + }); + + test('should update an array of string fields (ComboBox)', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + + ); + }; + + registerTestBed(TestComp)(); + + if (formHook === null) { + throw new Error('Formhook has not been set.'); + } + + expect(formHook.getFormData()).toEqual({ + tags: ['foo', 'bar'], + }); + + const newFormData = { + tags: ['updated', 'array'], + }; + + await act(async () => { + formHook!.updateFieldValues(newFormData); + }); + + expect(formHook.getFormData()).toEqual(newFormData); + }); + + test('should update recursively an array of object fields', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + {({ items: userItems }) => ( + <> + {userItems.map(({ id: userId, path: userPath }) => ( +
+ + + {({ items: addressItems }) => ( + <> + {addressItems.map( + ({ id: addressId, path: addressPath, isNew: isNewAddress }) => ( +
+ + +
+ ) + )} + + )} +
+ +
+ ))} + + )} +
+
+ ); + }; + + registerTestBed(TestComp)(); + + if (formHook === null) { + throw new Error('Formhook has not been set.'); + } + + expect(formHook.getFormData()).toEqual({ + users: [ + { + name: 'John', + address: [ + { + street: 'Street name', + city: 'Lagos', + }, + ], + tags: ['blue', 'red'], + }, + ], + }); + + const newFormData = { + users: [ + { + name: 'Balbina', + tags: ['yellow', 'pink'], + address: [ + { + street: 'Rua direita', + city: 'Burgau', + }, + ], + }, + { + name: 'Mike', + tags: ['green', 'black', 'orange'], + address: [ + { + street: 'Calle de Callao', + city: 'Madrid', + }, + { + street: 'Rue de Flagey', + city: 'Brussels', + }, + ], + }, + ], + }; + + await act(async () => { + formHook!.updateFieldValues(newFormData); + }); + + expect(formHook.getFormData()).toEqual(newFormData); + }); + + describe('deserializer', () => { + const formDefaultValue = { foo: 'initial' }; + const deserializer = (formData: typeof formDefaultValue) => ({ + foo: { label: formData.foo.toUpperCase(), value: formData.foo }, + }); + + const TestComp = () => { + const { form } = useForm({ defaultValue: formDefaultValue, deserializer }); + formHook = form; + + return ( +
+ {() => null} +
+ ); + }; + + test('should run deserializer on the new form data provided', async () => { + registerTestBed(TestComp)(); + + if (formHook === null) { + throw new Error('Formhook has not been set.'); + } + + expect(formHook.getFormData()).toEqual({ + foo: { label: 'INITIAL', value: 'initial' }, + }); + + const newFormData = { + foo: 'updated', + }; + + await act(async () => { + formHook!.updateFieldValues(newFormData); + }); + + expect(formHook.getFormData()).toEqual({ + foo: { label: 'UPDATED', value: 'updated' }, + }); + }); + + test('should not run deserializer on the new form data provided', async () => { + registerTestBed(TestComp)(); + + if (formHook === null) { + throw new Error('Formhook has not been set.'); + } + + expect(formHook.getFormData()).toEqual({ + foo: { label: 'INITIAL', value: 'initial' }, + }); + + const newFormData = { + foo: 'updated', + }; + + await act(async () => { + formHook!.updateFieldValues(newFormData, { runDeserializer: false }); + }); + + expect(formHook.getFormData()).toEqual({ + foo: 'updated', + }); + }); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 3966f9cc61a70..b6b45c76e7115 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -7,11 +7,19 @@ */ import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; -import { get } from 'lodash'; +import { get, mergeWith } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; import { FormHook, FieldHook, FormData, FieldsMap, FormConfig } from '../types'; -import { mapFormFields, unflattenObject, flattenObject, Subject, Subscription } from '../lib'; +import { + mapFormFields, + unflattenObject, + flattenObject, + stripOutUndefinedValues, + Subject, + Subscription, +} from '../lib'; +import { createArrayItem, getInternalArrayFieldPath } from '../components/use_array'; const DEFAULT_OPTIONS = { valueChangeDebounceTime: 500, @@ -37,23 +45,23 @@ export function useForm( // Strip out any "undefined" value and run the deserializer const initDefaultValue = useCallback( - (_defaultValue?: Partial): I | undefined => { + (_defaultValue?: Partial, runDeserializer: boolean = true): I | undefined => { if (_defaultValue === undefined || Object.keys(_defaultValue).length === 0) { return undefined; } - const filtered = Object.entries(_defaultValue as object) - .filter(({ 1: value }) => value !== undefined) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {} as T); + const filtered = stripOutUndefinedValues(_defaultValue); - return deserializer ? deserializer(filtered) : (filtered as unknown as I); + return runDeserializer && deserializer + ? stripOutUndefinedValues(deserializer(filtered)) + : (filtered as unknown as I); }, [deserializer] ); // We create this stable reference to be able to initialize our "defaultValueDeserialized" ref below // as we can't initialize useRef by calling a function (e.g. useRef(initDefaultValue())) - const defaultValueMemoized = useMemo(() => { + const defaultValueInitialized = useMemo(() => { return initDefaultValue(defaultValue); }, [defaultValue, initDefaultValue]); @@ -91,7 +99,7 @@ export function useForm( * Keep a reference to the form defaultValue once it has been deserialized. * This allows us to reset the form and put back the initial value of each fields */ - const defaultValueDeserialized = useRef(defaultValueMemoized); + const defaultValueDeserialized = useRef(defaultValueInitialized); /** * We have both a state and a ref for the error messages so the consumer can, in the same callback, @@ -440,6 +448,77 @@ export function useForm( [] ); + const updateFieldValues: FormHook['updateFieldValues'] = useCallback( + (updatedFormData, { runDeserializer = true } = {}) => { + if ( + !updatedFormData || + typeof updatedFormData !== 'object' || + Object.keys(updatedFormData).length === 0 + ) { + return; + } + + const updatedFormDataInitialized = initDefaultValue(updatedFormData, runDeserializer); + + const mergedDefaultValue = mergeWith( + {}, + defaultValueDeserialized.current, + updatedFormDataInitialized, + (_, srcValue) => { + if (Array.isArray(srcValue)) { + // Arrays are returned as provided, we don't want to merge + // previous array values with the new ones. + return srcValue; + } + } + ); + + defaultValueDeserialized.current = stripOutUndefinedValues(mergedDefaultValue); + + const doUpdateValues = (obj: object, currentObjPath: string[] = []) => { + Object.entries(obj).forEach(([key, value]) => { + const fullPath = [...currentObjPath, key].join('.'); + const internalArrayfieldPath = getInternalArrayFieldPath(fullPath); + + // Check if there is an **internal array** (created by ) defined at this key. + // If there is one, we update that field value and don't go any further as from there it will + // be the individual fields (children) declared inside the UseArray that will read the "defaultValue" + // object of the form (which we've updated above). + if (Array.isArray(value) && fieldsRefs.current[internalArrayfieldPath]) { + const field = fieldsRefs.current[internalArrayfieldPath]; + const fieldValue = value.map((_, index) => createArrayItem(fullPath, index, false)); + field.setValue(fieldValue); + return; + } + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // We make sure that at least _some_ leaf fields are present in the fieldsRefs object + // If not, we should not consider this as a multi fields but single field (e.g. a select field whose value is { label: 'Foo', value: 'foo' }) + const hasSomeLeafField = Object.keys(value).some( + (leaf) => fieldsRefs.current[`${fullPath}.${leaf}`] !== undefined + ); + + if (hasSomeLeafField) { + // Recursively update internal objects + doUpdateValues(value, [...currentObjPath, key]); + return; + } + } + + const field = fieldsRefs.current[fullPath]; + if (!field) { + return; + } + + field.setValue(value); + }); + }; + + doUpdateValues(updatedFormDataInitialized!); + }, + [initDefaultValue] + ); + const submit: FormHook['submit'] = useCallback( async (e) => { if (e) { @@ -536,6 +615,7 @@ export function useForm( getFieldDefaultValue, getFormData, getErrors, + updateFieldValues, reset, validateFields, __options: formOptions, @@ -563,6 +643,7 @@ export function useForm( getErrors, getFormDefaultValue, getFieldDefaultValue, + updateFieldValues, reset, formOptions, getFormData$, @@ -578,16 +659,6 @@ export function useForm( // ---------------------------------- // -- EFFECTS // ---------------------------------- - - useEffect(() => { - if (!isMounted.current) { - return; - } - - // Whenever the "defaultValue" prop changes, reinitialize our ref - defaultValueDeserialized.current = defaultValueMemoized; - }, [defaultValueMemoized]); - useEffect(() => { isMounted.current = true; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/index.ts index b65dc0570acba..2bdf942c38d3a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/index.ts @@ -10,4 +10,4 @@ export type { Subscription } from './subject'; export { Subject } from './subject'; -export { flattenObject, unflattenObject, mapFormFields } from './utils'; +export { flattenObject, unflattenObject, mapFormFields, stripOutUndefinedValues } from './utils'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.test.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.test.ts index f7d7429889eb2..df17700fc8c44 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.test.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { flattenObject } from './utils'; +import { flattenObject, stripOutUndefinedValues } from './utils'; describe('Form lib utils', () => { describe('flattenObject', () => { @@ -40,4 +40,28 @@ describe('Form lib utils', () => { }); }); }); + + describe('stripOutUndefinedValues', () => { + test('should remove all undefined values', () => { + const obj = { + foo: undefined, + bar: { + a: true, + b: undefined, + c: ['foo', undefined, 'bar'], + d: { + d: undefined, + }, + }, + }; + + expect(stripOutUndefinedValues(obj)).toEqual({ + bar: { + a: true, + c: ['foo', undefined, 'bar'], + d: {}, + }, + }); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 8df6506ec2e7b..54f6726abb115 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -56,6 +56,24 @@ export const flattenObject = ( return acc; }, {}); +/** + * Deeply remove all "undefined" value inside an Object + * + * @param obj The object to process + * @returns The object without any "undefined" + */ +export const stripOutUndefinedValues = (obj: GenericObject): R => { + return Object.entries(obj) + .filter(({ 1: value }) => value !== undefined) + .reduce((acc, [key, value]) => { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return { ...acc, [key]: stripOutUndefinedValues(value) }; + } + + return { ...acc, [key]: value }; + }, {} as R); +}; + /** * Helper to map the object of fields to any of its value * diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 80af60619a4e8..22b9fdc6229a7 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -42,6 +42,30 @@ export interface FormHook getFormData: () => T; /* Returns an array with of all errors in the form. */ getErrors: () => string[]; + /** + * Update multiple field values at once. You don't need to provide all the form + * fields, **partial** update is supported. This method is mainly useful to update an array + * of object fields. + * + * @example + * ```js + * // Update an array of fields + * form.updateFieldValues({ myArray: [{ foo: 'bar', baz: true }, { foo2: 'bar2', baz: false }] }) + * + * // or simply multiple fields at once + * form.updateFieldValues({ foo: 'bar', baz: false }) + * ``` + */ + updateFieldValues: ( + updatedFormData: Partial & FormData, + options?: { + /** + * Flag to indicate if the deserializer(s) are run against the provided form data. + * @default true + */ + runDeserializer?: boolean; + } + ) => void; /** * Reset the form states to their initial value and optionally * all the fields to their initial values. From 13e1290525c06cbcd7580d42c7e5867da9791c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 19 Apr 2022 12:42:10 +0100 Subject: [PATCH 2/2] Improve comment --- .../static/forms/hook_form_lib/components/use_array.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts index 27a51467c5bda..d6ada976c875c 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -46,8 +46,9 @@ export const createArrayItem = (path: string, index: number, isNew = true): Arra /** * We create an internal field to represent the Array items. This field is not returned - * as part as the form data but is used internally to run validation on the array items - * and its value (an array of ArrayItem) is used to map to actual form fields. + * as part as the form data but is used internally to run validation on the array items. + * It is this internal field value (ArrayItem[]) that we then map to actual form fields + * (in the children func {({ items }) => (...)}) * * @param path The array path in the form data * @returns The internal array field path