[Form lib] Export internal state instead of raw state (#80842)

This commit is contained in:
Sébastien Loix 2020-10-20 13:51:11 +02:00 committed by GitHub
parent 08a6ddf25b
commit 702e0c7d73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 449 additions and 377 deletions

View file

@ -133,7 +133,7 @@ describe('<FormDataProvider />', () => {
find('btn').simulate('click').update();
});
expect(onFormData.mock.calls.length).toBe(1);
expect(onFormData.mock.calls.length).toBe(2);
const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters<
OnUpdateHandler

View file

@ -22,13 +22,16 @@ import React from 'react';
import { FormData } from '../types';
import { useFormData } from '../hooks';
interface Props {
children: (formData: FormData) => JSX.Element | null;
interface Props<I> {
children: (formData: I) => JSX.Element | null;
pathsToWatch?: string | string[];
}
export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch });
const FormDataProviderComp = function <I extends FormData = FormData>({
children,
pathsToWatch,
}: Props<I>) {
const { 0: formData, 2: isReady } = useFormData<I>({ watch: pathsToWatch });
if (!isReady) {
// No field has mounted yet, don't render anything
@ -36,4 +39,6 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) =
}
return children(formData);
});
};
export const FormDataProvider = React.memo(FormDataProviderComp) as typeof FormDataProviderComp;

View file

@ -107,7 +107,7 @@ export const UseArray = ({
getNewItemAtIndex,
]);
// Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data.
// Create a new hook field with the "isIncludedInOutput" set to false so we don't use its value to build the final form data.
// Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle.
const fieldConfigBase: FieldConfig<ArrayItem[]> & InternalFieldConfig<ArrayItem[]> = {
defaultValue: fieldDefaultValue,

View file

@ -58,7 +58,7 @@ describe('<UseField />', () => {
OnUpdateHandler
>;
expect(data.raw).toEqual({
expect(data.internal).toEqual({
name: 'John',
lastName: 'Snow',
});
@ -214,8 +214,8 @@ describe('<UseField />', () => {
expect(serializer).not.toBeCalled();
expect(formatter).not.toBeCalled();
let formData = formHook.getFormData({ unflatten: false });
expect(formData.name).toEqual('John-deserialized');
const internalFormData = formHook.__getFormData$().value;
expect(internalFormData.name).toEqual('John-deserialized');
await act(async () => {
form.setInputValue('myField', 'Mike');
@ -224,9 +224,9 @@ describe('<UseField />', () => {
expect(formatter).toBeCalled(); // Formatters are executed on each value change
expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data
formData = formHook.getFormData();
const outputtedFormData = formHook.getFormData();
expect(serializer).toBeCalled();
expect(formData.name).toEqual('MIKE-serialized');
expect(outputtedFormData.name).toEqual('MIKE-serialized');
// Make sure that when we reset the form values, we don't serialize the fields
serializer.mockReset();

View file

@ -22,9 +22,9 @@ import React, { createContext, useContext, useMemo } from 'react';
import { FormData, FormHook } from './types';
import { Subject } from './lib';
export interface Context<T extends FormData = FormData, I = T> {
getFormData$: () => Subject<I>;
getFormData: FormHook<T>['getFormData'];
export interface Context<T extends FormData = FormData, I extends FormData = T> {
getFormData$: () => Subject<FormData>;
getFormData: FormHook<T, I>['getFormData'];
}
const FormDataContext = createContext<Context<any> | undefined>(undefined);
@ -45,6 +45,6 @@ export const FormDataContextProvider = ({ children, getFormData$, getFormData }:
return <FormDataContext.Provider value={value}>{children}</FormDataContext.Provider>;
};
export function useFormDataContext<T extends FormData = FormData>() {
return useContext<Context<T> | undefined>(FormDataContext);
export function useFormDataContext<T extends FormData = FormData, I extends FormData = T>() {
return useContext<Context<T, I> | undefined>(FormDataContext);
}

View file

@ -63,6 +63,7 @@ export const useField = <T, FormType = FormData, I = T>(
__removeField,
__updateFormDataAt,
__validateFields,
__getFormData$,
} = form;
const deserializeValue = useCallback(
@ -76,7 +77,7 @@ export const useField = <T, FormType = FormData, I = T>(
);
const [value, setStateValue] = useState<I>(deserializeValue);
const [errors, setErrors] = useState<ValidationError[]>([]);
const [errors, setStateErrors] = useState<ValidationError[]>([]);
const [isPristine, setPristine] = useState(true);
const [isValidating, setValidating] = useState(false);
const [isChangingValue, setIsChangingValue] = useState(false);
@ -86,18 +87,12 @@ export const useField = <T, FormType = FormData, I = T>(
const validateCounter = useRef(0);
const changeCounter = useRef(0);
const hasBeenReset = useRef<boolean>(false);
const inflightValidation = useRef<Promise<any> | null>(null);
const inflightValidation = useRef<(Promise<any> & { cancel?(): void }) | null>(null);
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
// ----------------------------------
// -- HELPERS
// ----------------------------------
const serializeValue: FieldHook<T, I>['__serializeValue'] = useCallback(
(internalValue: I = value) => {
return serializer ? serializer(internalValue) : ((internalValue as unknown) as T);
},
[serializer, value]
);
/**
* Filter an array of errors with specific validation type on them
*
@ -117,6 +112,11 @@ export const useField = <T, FormType = FormData, I = T>(
);
};
/**
* If the field has some "formatters" defined in its config, run them in series and return
* the transformed value. This handler is called whenever the field value changes, right before
* updating the "value" state.
*/
const formatInputValue = useCallback(
<T>(inputValue: unknown): T => {
const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === '';
@ -125,11 +125,11 @@ export const useField = <T, FormType = FormData, I = T>(
return inputValue as T;
}
const formData = getFormData({ unflatten: false });
const formData = __getFormData$().value;
return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T;
},
[formatters, getFormData]
[formatters, __getFormData$]
);
const onValueChange = useCallback(async () => {
@ -147,7 +147,7 @@ export const useField = <T, FormType = FormData, I = T>(
// Update the form data observable
__updateFormDataAt(path, value);
// Validate field(s) (that will update form.isValid state)
// Validate field(s) (this will update the form.isValid state)
await __validateFields(fieldsToValidateOnChange ?? [path]);
if (isMounted.current === false) {
@ -162,15 +162,18 @@ export const useField = <T, FormType = FormData, I = T>(
*/
if (changeIteration === changeCounter.current) {
if (valueChangeDebounceTime > 0) {
const delta = Date.now() - startTime;
if (delta < valueChangeDebounceTime) {
const timeElapsed = Date.now() - startTime;
if (timeElapsed < valueChangeDebounceTime) {
const timeLeftToWait = valueChangeDebounceTime - timeElapsed;
debounceTimeout.current = setTimeout(() => {
debounceTimeout.current = null;
setIsChangingValue(false);
}, valueChangeDebounceTime - delta);
}, timeLeftToWait);
return;
}
}
setIsChangingValue(false);
}
}, [
@ -183,41 +186,34 @@ export const useField = <T, FormType = FormData, I = T>(
__validateFields,
]);
// Cancel any inflight validation (e.g an HTTP Request)
const cancelInflightValidation = useCallback(() => {
// Cancel any inflight validation (like an HTTP Request)
if (
inflightValidation.current &&
typeof (inflightValidation.current as any).cancel === 'function'
) {
(inflightValidation.current as any).cancel();
if (inflightValidation.current && typeof inflightValidation.current.cancel === 'function') {
inflightValidation.current.cancel();
inflightValidation.current = null;
}
}, []);
const clearErrors: FieldHook['clearErrors'] = useCallback(
(validationType = VALIDATION_TYPES.FIELD) => {
setErrors((previousErrors) => filterErrors(previousErrors, validationType));
},
[]
);
const runValidations = useCallback(
({
formData,
value: valueToValidate,
validationTypeToValidate,
}: {
formData: any;
value: I;
validationTypeToValidate?: string;
}): ValidationError[] | Promise<ValidationError[]> => {
(
{
formData,
value: valueToValidate,
validationTypeToValidate,
}: {
formData: any;
value: I;
validationTypeToValidate?: string;
},
clearFieldErrors: FieldHook['clearErrors']
): ValidationError[] | Promise<ValidationError[]> => {
if (!validations) {
return [];
}
// By default, for fields that have an asynchronous validation
// we will clear the errors as soon as the field value changes.
clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
cancelInflightValidation();
@ -329,21 +325,33 @@ export const useField = <T, FormType = FormData, I = T>(
// We first try to run the validations synchronously
return runSync();
},
[clearErrors, cancelInflightValidation, validations, getFormData, getFields, path]
[cancelInflightValidation, validations, getFormData, getFields, path]
);
// -- API
// ----------------------------------
// -- Internal API
// ----------------------------------
const serializeValue: FieldHook<T, I>['__serializeValue'] = useCallback(
(internalValue: I = value) => {
return serializer ? serializer(internalValue) : ((internalValue as unknown) as T);
},
[serializer, value]
);
// ----------------------------------
// -- Public API
// ----------------------------------
const clearErrors: FieldHook['clearErrors'] = useCallback(
(validationType = VALIDATION_TYPES.FIELD) => {
setStateErrors((previousErrors) => filterErrors(previousErrors, validationType));
},
[]
);
/**
* Validate a form field, running all its validations.
* If a validationType is provided then only that validation will be executed,
* skipping the other type of validation that might exist.
*/
const validate: FieldHook<T, I>['validate'] = useCallback(
(validationData = {}) => {
const {
formData = getFormData({ unflatten: false }),
formData = __getFormData$().value,
value: valueToValidate = value,
validationType,
} = validationData;
@ -362,7 +370,7 @@ export const useField = <T, FormType = FormData, I = T>(
// This is the most recent invocation
setValidating(false);
// Update the errors array
setErrors((prev) => {
setStateErrors((prev) => {
const filteredErrors = filterErrors(prev, validationType);
return [...filteredErrors, ..._validationErrors];
});
@ -374,25 +382,23 @@ export const useField = <T, FormType = FormData, I = T>(
};
};
const validationErrors = runValidations({
formData,
value: valueToValidate,
validationTypeToValidate: validationType,
});
const validationErrors = runValidations(
{
formData,
value: valueToValidate,
validationTypeToValidate: validationType,
},
clearErrors
);
if (Reflect.has(validationErrors, 'then')) {
return (validationErrors as Promise<ValidationError[]>).then(onValidationResult);
}
return onValidationResult(validationErrors as ValidationError[]);
},
[getFormData, value, runValidations]
[__getFormData$, value, runValidations, clearErrors]
);
/**
* Handler to change the field value
*
* @param newValue The new value to assign to the field
*/
const setValue: FieldHook<T, I>['setValue'] = useCallback(
(newValue) => {
setStateValue((prev) => {
@ -408,8 +414,8 @@ export const useField = <T, FormType = FormData, I = T>(
[formatInputValue]
);
const _setErrors: FieldHook<T, I>['setErrors'] = useCallback((_errors) => {
setErrors(
const setErrors: FieldHook<T, I>['setErrors'] = useCallback((_errors) => {
setStateErrors(
_errors.map((error) => ({
validationType: VALIDATION_TYPES.FIELD,
__isBlocking__: true,
@ -418,11 +424,6 @@ export const useField = <T, FormType = FormData, I = T>(
);
}, []);
/**
* Form <input /> "onChange" event handler
*
* @param event Form input change event
*/
const onChange: FieldHook<T, I>['onChange'] = useCallback(
(event) => {
const newValue = {}.hasOwnProperty.call(event!.target, 'checked')
@ -485,7 +486,7 @@ export const useField = <T, FormType = FormData, I = T>(
case 'value':
return setValue(nextValue);
case 'errors':
return setErrors(nextValue);
return setStateErrors(nextValue);
case 'isChangingValue':
return setIsChangingValue(nextValue);
case 'isPristine':
@ -539,7 +540,7 @@ export const useField = <T, FormType = FormData, I = T>(
onChange,
getErrorsMessages,
setValue,
setErrors: _setErrors,
setErrors,
clearErrors,
validate,
reset,
@ -563,7 +564,7 @@ export const useField = <T, FormType = FormData, I = T>(
onChange,
getErrorsMessages,
setValue,
_setErrors,
setErrors,
clearErrors,
validate,
reset,
@ -585,7 +586,8 @@ export const useField = <T, FormType = FormData, I = T>(
useEffect(() => {
// If the field value has been reset, we don't want to call the "onValueChange()"
// as it will set the "isPristine" state to true or validate the field, which initially we don't want.
// as it will set the "isPristine" state to true or validate the field, which we don't want
// to occur right after resetting the field state.
if (hasBeenReset.current) {
hasBeenReset.current = false;
return;

View file

@ -211,7 +211,13 @@ describe('useForm() hook', () => {
test('should allow subscribing to the form data changes and provide a handler to build the form data', async () => {
const TestComp = ({ onData }: { onData: OnUpdateHandler }) => {
const { form } = useForm();
const { form } = useForm({
serializer: (value) => ({
user: {
name: value.user.name.toUpperCase(),
},
}),
});
const { subscribe } = form;
useEffect(() => {
@ -253,8 +259,9 @@ describe('useForm() hook', () => {
OnUpdateHandler
>;
expect(data.raw).toEqual({ 'user.name': 'John' });
expect(data.format()).toEqual({ user: { name: 'John' } });
expect(data.internal).toEqual({ user: { name: 'John' } });
// Transform name to uppercase as decalred in our serializer func
expect(data.format()).toEqual({ user: { name: 'JOHN' } });
// As we have touched all fields, the validity went from "undefined" to "true"
expect(isValid).toBe(true);
});
@ -302,10 +309,12 @@ describe('useForm() hook', () => {
OnUpdateHandler
>;
expect(data.raw).toEqual({
expect(data.internal).toEqual({
title: defaultValue.title,
subTitle: 'hasBeenOverridden',
'user.name': defaultValue.user.name,
user: {
name: defaultValue.user.name,
},
});
});
});

View file

@ -58,8 +58,6 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
return initDefaultValue(defaultValue);
}, [defaultValue, initDefaultValue]);
const defaultValueDeserialized = useRef(defaultValueMemoized);
const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields } = options ?? {};
const formOptions = useMemo(
() => ({
@ -72,26 +70,36 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [isValid, setIsValid] = useState<boolean | undefined>(undefined);
const fieldsRefs = useRef<FieldsMap>({});
const formUpdateSubscribers = useRef<Subscription[]>([]);
const isMounted = useRef<boolean>(false);
const defaultValueDeserialized = useRef(defaultValueMemoized);
// formData$ is an observable we can subscribe to in order to receive live
// update of the raw form data. As an observable it does not trigger any React
// render().
// The <FormDataProvider> component is the one in charge of reading this observable
// and updating its state to trigger the necessary view render.
const formData$ = useRef<Subject<T> | null>(null);
// The "useFormData()" hook is the one in charge of reading this observable
// and updating its own state that will trigger the necessary re-renders in the UI.
const formData$ = useRef<Subject<FormData> | null>(null);
// ----------------------------------
// -- HELPERS
// ----------------------------------
const getFormData$ = useCallback((): Subject<T> => {
const getFormData$ = useCallback((): Subject<FormData> => {
if (formData$.current === null) {
formData$.current = new Subject<T>({} as T);
formData$.current = new Subject<FormData>({});
}
return formData$.current;
}, []);
const updateFormData$ = useCallback(
(nextValue: FormData) => {
getFormData$().next(nextValue);
},
[getFormData$]
);
const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []);
const getFieldsForOutput = useCallback(
@ -115,63 +123,24 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
[]
);
const updateFormDataAt: FormHook<T>['__updateFormDataAt'] = useCallback(
const updateFormDataAt: FormHook<T, I>['__updateFormDataAt'] = useCallback(
(path, value) => {
const _formData$ = getFormData$();
const currentFormData = _formData$.value;
const currentFormData = getFormData$().value;
if (currentFormData[path] !== value) {
_formData$.next({ ...currentFormData, [path]: value });
updateFormData$({ ...currentFormData, [path]: value });
}
return _formData$.value;
},
[getFormData$]
[getFormData$, updateFormData$]
);
const updateDefaultValueAt: FormHook<T>['__updateDefaultValueAt'] = useCallback((path, value) => {
set(defaultValueDeserialized.current, path, value);
}, []);
// -- API
// ----------------------------------
const getFormData: FormHook<T>['getFormData'] = useCallback(
(getDataOptions: Parameters<FormHook<T>['getFormData']>[0] = { unflatten: true }) => {
if (getDataOptions.unflatten) {
const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, {
stripEmptyFields: formOptions.stripEmptyFields,
});
const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue());
return serializer
? (serializer(unflattenObject(fieldsValue) as I) as T)
: (unflattenObject(fieldsValue) as T);
}
return Object.entries(fieldsRefs.current).reduce(
(acc, [key, field]) => ({
...acc,
[key]: field.value,
}),
{} as T
);
const updateDefaultValueAt: FormHook<T, I>['__updateDefaultValueAt'] = useCallback(
(path, value) => {
set(defaultValueDeserialized.current, path, value);
},
[getFieldsForOutput, formOptions.stripEmptyFields, serializer]
[]
);
const getErrors: FormHook['getErrors'] = useCallback(() => {
if (isValid === true) {
return [];
}
return fieldsToArray().reduce((acc, field) => {
const fieldError = field.getErrorsMessages();
if (fieldError === null) {
return acc;
}
return [...acc, fieldError];
}, [] as string[]);
}, [isValid, fieldsToArray]);
const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating;
const waitForFieldsToFinishValidating = useCallback(async () => {
@ -192,13 +161,13 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
});
}, [fieldsToArray]);
const validateFields: FormHook<T>['__validateFields'] = useCallback(
const validateFields: FormHook<T, I>['__validateFields'] = useCallback(
async (fieldNames) => {
const fieldsToValidate = fieldNames
.map((name) => fieldsRefs.current[name])
.filter((field) => field !== undefined);
const formData = getFormData({ unflatten: false });
const formData = getFormData$().value;
const validationResult = await Promise.all(
fieldsToValidate.map((field) => field.validate({ formData }))
);
@ -245,10 +214,101 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
return { areFieldsValid, isFormValid };
},
[getFormData, fieldsToArray]
[getFormData$, fieldsToArray]
);
const validateAllFields = useCallback(async (): Promise<boolean> => {
// ----------------------------------
// -- Internal API
// ----------------------------------
const addField: FormHook<T, I>['__addField'] = useCallback(
(field) => {
fieldsRefs.current[field.path] = field;
updateFormDataAt(field.path, field.value);
if (!field.isValidated) {
setIsValid(undefined);
// When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**.
// If a field is added and it is not validated it means that we have swapped fields and added new ones:
// --> we have basically have a new form in front of us.
// For that reason we make sure that the "isSubmitted" state is false.
setIsSubmitted(false);
}
},
[updateFormDataAt]
);
const removeField: FormHook<T, I>['__removeField'] = useCallback(
(_fieldNames) => {
const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames];
const currentFormData = { ...getFormData$().value };
fieldNames.forEach((name) => {
delete fieldsRefs.current[name];
delete currentFormData[name];
});
updateFormData$(currentFormData);
/**
* After removing a field, the form validity might have changed
* (an invalid field might have been removed and now the form is valid)
*/
setIsValid((prev) => {
if (prev === false) {
const isFormValid = fieldsToArray().every(isFieldValid);
return isFormValid;
}
// If the form validity is "true" or "undefined", it does not change after removing a field
return prev;
});
},
[getFormData$, updateFormData$, fieldsToArray]
);
const getFieldDefaultValue: FormHook<T, I>['__getFieldDefaultValue'] = useCallback(
(fieldName) => get(defaultValueDeserialized.current, fieldName),
[]
);
const readFieldConfigFromSchema: FormHook<T, I>['__readFieldConfigFromSchema'] = useCallback(
(fieldName) => {
const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
return config;
},
[schema]
);
// ----------------------------------
// -- Public API
// ----------------------------------
const getFormData: FormHook<T, I>['getFormData'] = useCallback(() => {
const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, {
stripEmptyFields: formOptions.stripEmptyFields,
});
const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue());
return serializer
? serializer(unflattenObject<I>(fieldsValue))
: unflattenObject<T>(fieldsValue);
}, [getFieldsForOutput, formOptions.stripEmptyFields, serializer]);
const getErrors: FormHook<T, I>['getErrors'] = useCallback(() => {
if (isValid === true) {
return [];
}
return fieldsToArray().reduce((acc, field) => {
const fieldError = field.getErrorsMessages();
if (fieldError === null) {
return acc;
}
return [...acc, fieldError];
}, [] as string[]);
}, [isValid, fieldsToArray]);
const validate: FormHook<T, I>['validate'] = useCallback(async (): Promise<boolean> => {
// Maybe some field are being validated because of their async validation(s).
// We make sure those validations have finished executing before proceeding.
await waitForFieldsToFinishValidating();
@ -272,84 +332,23 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
return isFormValid!;
}, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]);
const addField: FormHook<T>['__addField'] = useCallback(
(field) => {
fieldsRefs.current[field.path] = field;
updateFormDataAt(field.path, field.value);
if (!field.isValidated) {
setIsValid(undefined);
// When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**.
// If a field is added and it is not validated it means that we have swapped fields and added new ones:
// --> we have basically have a new form in front of us.
// For that reason we make sure that the "isSubmitted" state is false.
setIsSubmitted(false);
}
},
[updateFormDataAt]
);
const removeField: FormHook<T>['__removeField'] = useCallback(
(_fieldNames) => {
const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames];
const currentFormData = { ...getFormData$().value } as FormData;
fieldNames.forEach((name) => {
delete fieldsRefs.current[name];
delete currentFormData[name];
});
getFormData$().next(currentFormData as T);
/**
* After removing a field, the form validity might have changed
* (an invalid field might have been removed and now the form is valid)
*/
setIsValid((prev) => {
if (prev === false) {
const isFormValid = fieldsToArray().every(isFieldValid);
return isFormValid;
}
// If the form validity is "true" or "undefined", it does not change after removing a field
return prev;
});
},
[getFormData$, fieldsToArray]
);
const setFieldValue: FormHook<T>['setFieldValue'] = useCallback((fieldName, value) => {
const setFieldValue: FormHook<T, I>['setFieldValue'] = useCallback((fieldName, value) => {
if (fieldsRefs.current[fieldName] === undefined) {
return;
}
fieldsRefs.current[fieldName].setValue(value);
}, []);
const setFieldErrors: FormHook<T>['setFieldErrors'] = useCallback((fieldName, errors) => {
const setFieldErrors: FormHook<T, I>['setFieldErrors'] = useCallback((fieldName, errors) => {
if (fieldsRefs.current[fieldName] === undefined) {
return;
}
fieldsRefs.current[fieldName].setErrors(errors);
}, []);
const getFields: FormHook<T>['getFields'] = useCallback(() => fieldsRefs.current, []);
const getFields: FormHook<T, I>['getFields'] = useCallback(() => fieldsRefs.current, []);
const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback(
(fieldName) => get(defaultValueDeserialized.current, fieldName),
[]
);
const readFieldConfigFromSchema: FormHook<T>['__readFieldConfigFromSchema'] = useCallback(
(fieldName) => {
const config = (get(schema ?? {}, fieldName) as FieldConfig) || {};
return config;
},
[schema]
);
const submitForm: FormHook<T>['submit'] = useCallback(
const submit: FormHook<T, I>['submit'] = useCallback(
async (e) => {
if (e) {
e.preventDefault();
@ -358,7 +357,7 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
setIsSubmitted(true); // User has attempted to submit the form at least once
setSubmitting(true);
const isFormValid = await validateAllFields();
const isFormValid = await validate();
const formData = isFormValid ? getFormData() : ({} as T);
if (onSubmit) {
@ -371,13 +370,17 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
return { data: formData, isValid: isFormValid! };
},
[validateAllFields, getFormData, onSubmit]
[validate, getFormData, onSubmit]
);
const subscribe: FormHook<T>['subscribe'] = useCallback(
const subscribe: FormHook<T, I>['subscribe'] = useCallback(
(handler) => {
const subscription = getFormData$().subscribe((raw) => {
handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields });
handler({
isValid,
data: { internal: unflattenObject<I>(raw), format: getFormData },
validate,
});
});
formUpdateSubscribers.current.push(subscription);
@ -391,17 +394,13 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
},
};
},
[getFormData$, isValid, getFormData, validateAllFields]
[getFormData$, isValid, getFormData, validate]
);
/**
* Reset all the fields of the form to their default values
* and reset all the states to their original value.
*/
const reset: FormHook<T>['reset'] = useCallback(
const reset: FormHook<T, I>['reset'] = useCallback(
(resetOptions = { resetValues: true }) => {
const { resetValues = true, defaultValue: updatedDefaultValue } = resetOptions;
const currentFormData = { ...getFormData$().value } as FormData;
const currentFormData = { ...getFormData$().value };
if (updatedDefaultValue) {
defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue);
@ -417,25 +416,26 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
currentFormData[path] = fieldDefaultValue;
}
});
if (resetValues) {
getFormData$().next(currentFormData as T);
updateFormData$(currentFormData);
}
setIsSubmitted(false);
setSubmitting(false);
setIsValid(undefined);
},
[getFormData$, initDefaultValue, getFieldDefaultValue]
[getFormData$, updateFormData$, initDefaultValue, getFieldDefaultValue]
);
const form = useMemo<FormHook<T>>(() => {
const form = useMemo<FormHook<T, I>>(() => {
return {
isSubmitted,
isSubmitting,
isValid,
id,
submit: submitForm,
validate: validateAllFields,
submit,
validate,
subscribe,
setFieldValue,
setFieldErrors,
@ -458,7 +458,7 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
isSubmitting,
isValid,
id,
submitForm,
submit,
subscribe,
setFieldValue,
setFieldErrors,
@ -475,7 +475,7 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
addField,
removeField,
validateFields,
validateAllFields,
validate,
]);
useEffect(() => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { act } from 'react-dom/test-utils';
import { registerTestBed, TestBed } from '../shared_imports';
@ -25,37 +25,59 @@ import { Form, UseField } from '../components';
import { useForm } from './use_form';
import { useFormData, HookReturn } from './use_form_data';
interface Props {
onChange(data: HookReturn): void;
interface Props<T extends object> {
onChange(data: HookReturn<T>): void;
watch?: string | string[];
}
interface Form1 {
title: string;
}
interface Form2 {
user: {
firstName: string;
lastName: string;
};
}
interface Form3 {
title: string;
subTitle: string;
}
describe('useFormData() hook', () => {
const HookListenerComp = React.memo(({ onChange, watch }: Props) => {
const hookValue = useFormData({ watch });
const HookListenerComp = function <T extends object>({ onChange, watch }: Props<T>) {
const hookValue = useFormData<T>({ watch });
const isMounted = useRef(false);
useEffect(() => {
onChange(hookValue);
if (isMounted.current) {
onChange(hookValue);
}
isMounted.current = true;
}, [hookValue, onChange]);
return null;
});
};
const HookListener = React.memo(HookListenerComp);
describe('form data updates', () => {
let testBed: TestBed;
let onChangeSpy: jest.Mock;
const getLastMockValue = () => {
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn<Form1>;
};
const TestComp = (props: Props) => {
const { form } = useForm();
const TestComp = (props: Props<Form1>) => {
const { form } = useForm<Form1>();
return (
<Form form={form}>
<UseField path="title" defaultValue="titleInitialValue" data-test-subj="titleField" />
<HookListenerComp {...props} />
<HookListener {...props} />
</Form>
);
};
@ -70,9 +92,7 @@ describe('useFormData() hook', () => {
});
test('should return the form data', () => {
// Called twice:
// once when the hook is called and once when the fields have mounted and updated the form data
expect(onChangeSpy).toBeCalledTimes(2);
expect(onChangeSpy).toBeCalledTimes(1);
const [data] = getLastMockValue();
expect(data).toEqual({ title: 'titleInitialValue' });
});
@ -86,7 +106,7 @@ describe('useFormData() hook', () => {
setInputValue('titleField', 'titleChanged');
});
expect(onChangeSpy).toBeCalledTimes(3);
expect(onChangeSpy).toBeCalledTimes(2);
const [data] = getLastMockValue();
expect(data).toEqual({ title: 'titleChanged' });
});
@ -96,17 +116,17 @@ describe('useFormData() hook', () => {
let onChangeSpy: jest.Mock;
const getLastMockValue = () => {
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn<Form2>;
};
const TestComp = (props: Props) => {
const { form } = useForm();
const TestComp = (props: Props<Form2>) => {
const { form } = useForm<Form2>();
return (
<Form form={form}>
<UseField path="user.firstName" defaultValue="John" />
<UseField path="user.lastName" defaultValue="Snow" />
<HookListenerComp {...props} />
<HookListener {...props} />
</Form>
);
};
@ -121,8 +141,8 @@ describe('useFormData() hook', () => {
});
test('should expose a handler to build the form data', () => {
const { 1: format } = getLastMockValue();
expect(format()).toEqual({
const [formData] = getLastMockValue();
expect(formData).toEqual({
user: {
firstName: 'John',
lastName: 'Snow',
@ -137,11 +157,11 @@ describe('useFormData() hook', () => {
let onChangeSpy: jest.Mock;
const getLastMockValue = () => {
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn<Form3>;
};
const TestComp = (props: Props) => {
const { form } = useForm();
const TestComp = (props: Props<Form3>) => {
const { form } = useForm<Form3>();
return (
<Form form={form}>
@ -190,9 +210,9 @@ describe('useFormData() hook', () => {
return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn;
};
const TestComp = ({ onChange }: Props) => {
const TestComp = ({ onChange }: Props<Form1>) => {
const { form } = useForm();
const hookValue = useFormData({ form });
const hookValue = useFormData<Form1>({ form });
useEffect(() => {
onChange(hookValue);

View file

@ -19,6 +19,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { FormData, FormHook } from '../types';
import { unflattenObject } from '../lib';
import { useFormDataContext, Context } from '../form_data_context';
interface Options {
@ -26,14 +27,16 @@ interface Options {
form?: FormHook<any>;
}
export type HookReturn<T extends object = FormData> = [FormData, () => T, boolean];
export type HookReturn<I extends object = FormData, T extends object = I> = [I, () => T, boolean];
export const useFormData = <T extends object = FormData>(options: Options = {}): HookReturn<T> => {
export const useFormData = <I extends object = FormData, T extends object = I>(
options: Options = {}
): HookReturn<I, T> => {
const { watch, form } = options;
const ctx = useFormDataContext<T>();
const ctx = useFormDataContext<T, I>();
let getFormData: Context<T>['getFormData'];
let getFormData$: Context<T>['getFormData$'];
let getFormData: Context<T, I>['getFormData'];
let getFormData$: Context<T, I>['getFormData$'];
if (form !== undefined) {
getFormData = form.getFormData;
@ -50,30 +53,33 @@ export const useFormData = <T extends object = FormData>(options: Options = {}):
const previousRawData = useRef<FormData>(initialValue);
const isMounted = useRef(false);
const [formData, setFormData] = useState<FormData>(previousRawData.current);
const [formData, setFormData] = useState<I>(() => unflattenObject<I>(previousRawData.current));
const formatFormData = useCallback(() => {
return getFormData({ unflatten: true });
}, [getFormData]);
/**
* We do want to offer to the consumer a handler to serialize the form data that changes each time
* the formData **state** changes. This is why we added the "formData" dep to the array and added the eslint override.
*/
const serializer = useCallback(() => {
return getFormData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getFormData, formData]);
useEffect(() => {
const subscription = getFormData$().subscribe((raw) => {
if (watch) {
const valuesToWatchArray = Array.isArray(watch)
? (watch as string[])
: ([watch] as string[]);
if (!isMounted.current && Object.keys(raw).length === 0) {
return;
}
if (
valuesToWatchArray.some(
(value) => previousRawData.current[value] !== raw[value as keyof T]
)
) {
if (watch) {
const pathsToWatchArray: string[] = Array.isArray(watch) ? watch : [watch];
if (pathsToWatchArray.some((path) => previousRawData.current[path] !== raw[path])) {
previousRawData.current = raw;
// Only update the state if one of the field we watch has changed.
setFormData(raw);
setFormData(unflattenObject<I>(raw));
}
} else {
setFormData(raw);
setFormData(unflattenObject<I>(raw));
}
});
return subscription.unsubscribe;
@ -88,8 +94,8 @@ export const useFormData = <T extends object = FormData>(options: Options = {}):
if (!isMounted.current && Object.keys(formData).length === 0) {
// No field has mounted yet
return [formData, formatFormData, false];
return [formData, serializer, false];
}
return [formData, formatFormData, true];
return [formData, serializer, true];
};

View file

@ -20,25 +20,11 @@
import { set } from '@elastic/safer-lodash-set';
import { FieldHook } from '../types';
export const unflattenObject = (object: any) =>
export const unflattenObject = <T extends object = { [key: string]: any }>(object: object): T =>
Object.entries(object).reduce((acc, [key, value]) => {
set(acc, key, value);
return acc;
}, {});
export const flattenObject = (
object: Record<string, any>,
to: Record<string, any> = {},
paths: string[] = []
): Record<string, any> =>
Object.entries(object).reduce((acc, [key, value]) => {
const updatedPaths = [...paths, key];
if (value !== null && !Array.isArray(value) && typeof value === 'object') {
return flattenObject(value, to, updatedPaths);
}
acc[updatedPaths.join('.')] = value;
return acc;
}, to);
}, {} as T);
/**
* Helper to map the object of fields to any of its value

View file

@ -40,33 +40,36 @@ export interface FormHook<T extends FormData = FormData, I extends FormData = T>
submit: (e?: FormEvent<HTMLFormElement> | MouseEvent) => Promise<{ data: T; isValid: boolean }>;
/** Use this handler to get the validity of the form. */
validate: () => Promise<boolean>;
subscribe: (handler: OnUpdateHandler<T>) => Subscription;
subscribe: (handler: OnUpdateHandler<T, I>) => Subscription;
/** Sets a field value imperatively. */
setFieldValue: (fieldName: string, value: FieldValue) => void;
/** Sets a field errors imperatively. */
setFieldErrors: (fieldName: string, errors: ValidationError[]) => void;
/** Access any field on the form. */
/** Access the fields on the form. */
getFields: () => FieldsMap;
/**
* Return the form data. It accepts an optional options object with an `unflatten` parameter (defaults to `true`).
* If you are only interested in the raw form data, pass `unflatten: false` to the handler
*/
getFormData: (options?: { unflatten?: boolean }) => T;
getFormData: () => T;
/* Returns an array with of all errors in the form. */
getErrors: () => string[];
/** Resets the form to its initial state. */
/**
* Reset the form states to their initial value and optionally
* all the fields to their initial values.
*/
reset: (options?: { resetValues?: boolean; defaultValue?: Partial<T> }) => void;
readonly __options: Required<FormOptions>;
__getFormData$: () => Subject<T>;
__getFormData$: () => Subject<FormData>;
__addField: (field: FieldHook) => void;
__removeField: (fieldNames: string | string[]) => void;
__validateFields: (
fieldNames: string[]
) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>;
__updateFormDataAt: (field: string, value: unknown) => T;
__updateFormDataAt: (field: string, value: unknown) => void;
__updateDefaultValueAt: (field: string, value: unknown) => void;
__readFieldConfigFromSchema: (fieldName: string) => FieldConfig;
__getFieldDefaultValue: (fieldName: string) => unknown;
__readFieldConfigFromSchema: (field: string) => FieldConfig;
__getFieldDefaultValue: (path: string) => unknown;
}
export type FormSchema<T extends FormData = FormData> = {
@ -83,16 +86,18 @@ export interface FormConfig<T extends FormData = FormData, I extends FormData =
id?: string;
}
export interface OnFormUpdateArg<T extends FormData> {
export interface OnFormUpdateArg<T extends FormData, I extends FormData = T> {
data: {
raw: { [key: string]: any };
internal: I;
format: () => T;
};
validate: () => Promise<boolean>;
isValid?: boolean;
}
export type OnUpdateHandler<T extends FormData = FormData> = (arg: OnFormUpdateArg<T>) => void;
export type OnUpdateHandler<T extends FormData = FormData, I extends FormData = T> = (
arg: OnFormUpdateArg<T, I>
) => void;
export interface FormOptions {
valueChangeDebounceTime?: number;
@ -119,10 +124,26 @@ export interface FieldHook<T = unknown, I = T> {
validationType?: 'field' | string;
errorCode?: string;
}) => string | null;
/**
* Form <input /> "onChange" event handler
*
* @param event Form input change event
*/
onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void;
/**
* Handler to change the field value
*
* @param value The new value to assign to the field. If you provide a callback, you wil receive
* the previous value and you need to return the next value.
*/
setValue: (value: I | ((prevValue: I) => I)) => void;
setErrors: (errors: ValidationError[]) => void;
clearErrors: (type?: string | string[]) => void;
/**
* Validate a form field, running all its validations.
* If a validationType is provided then only that validation will be executed,
* skipping the other type of validation that might exist.
*/
validate: (validateData?: {
formData?: any;
value?: I;
@ -166,19 +187,23 @@ export interface ValidationError<T = string> {
[key: string]: any;
}
export interface ValidationFuncArg<T extends FormData, V = unknown> {
export interface ValidationFuncArg<I extends FormData, V = unknown> {
path: string;
value: V;
form: {
getFormData: FormHook<T>['getFormData'];
getFields: FormHook<T>['getFields'];
getFormData: FormHook<FormData, I>['getFormData'];
getFields: FormHook<FormData, I>['getFields'];
};
formData: T;
formData: I;
errors: readonly ValidationError[];
}
export type ValidationFunc<T extends FormData = any, E extends string = string, V = unknown> = (
data: ValidationFuncArg<T, V>
export type ValidationFunc<
I extends FormData = FormData,
E extends string = string,
V = unknown
> = (
data: ValidationFuncArg<I, V>
) => ValidationError<E> | void | undefined | Promise<ValidationError<E> | void | undefined>;
export interface FieldValidateResponse {
@ -199,11 +224,11 @@ type FormatterFunc = (value: any, formData: FormData) => unknown;
type FieldValue = unknown;
export interface ValidationConfig<
FormType extends FormData = any,
Error extends string = string,
ValueType = unknown
I extends FormData = FormData,
E extends string = string,
V = unknown
> {
validator: ValidationFunc<FormType, Error, ValueType>;
validator: ValidationFunc<I, E, V>;
type?: string;
/**
* By default all validation are blockers, which means that if they fail, the field is invalid.

View file

@ -7,6 +7,7 @@
import React, { FunctionComponent, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui';
@ -56,10 +57,12 @@ export const ColdPhase: FunctionComponent<Props> = ({
errors,
isShowingErrors,
}) => {
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
const [formData] = useFormData({
watch: [useRolloverPath],
});
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
return (
<div id="coldPhaseContent" aria-live="polite" role="region">
<>

View file

@ -5,6 +5,7 @@
*/
import React, { FunctionComponent, Fragment } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
@ -48,10 +49,12 @@ export const DeletePhase: FunctionComponent<Props> = ({
isShowingErrors,
getUrlForApp,
}) => {
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
watch: [useRolloverPath],
const [formData] = useFormData({
watch: useRolloverPath,
});
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
return (
<div id="deletePhaseContent" aria-live="polite" role="region">
<EuiDescribedFormGroup

View file

@ -5,6 +5,7 @@
*/
import React, { Fragment, FunctionComponent, useEffect, useState } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
@ -45,8 +46,11 @@ const hotProperty: keyof Phases = 'hot';
export const HotPhase: FunctionComponent<{ setWarmPhaseOnRollover: (v: boolean) => void }> = ({
setWarmPhaseOnRollover,
}) => {
const [{ [useRolloverPath]: isRolloverEnabled }] = useFormData({ watch: [useRolloverPath] });
const form = useFormContext();
const [formData] = useFormData({
watch: useRolloverPath,
});
const isRolloverEnabled = get(formData, useRolloverPath);
const isShowingErrors = form.isValid === false;
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiSpacer, EuiTextColor } from '@elastic/eui';
import React from 'react';
@ -23,9 +23,11 @@ interface Props {
const forceMergeEnabledPath = '_meta.hot.forceMergeEnabled';
export const Forcemerge: React.FunctionComponent<Props> = ({ phase }) => {
const [{ [forceMergeEnabledPath]: forceMergeEnabled }] = useFormData({
watch: [forceMergeEnabledPath],
const [formData] = useFormData({
watch: forceMergeEnabledPath,
});
const forceMergeEnabled = get(formData, forceMergeEnabledPath);
return (
<EuiDescribedFormGroup
title={

View file

@ -5,6 +5,7 @@
*/
import React, { Fragment, FunctionComponent } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
@ -73,9 +74,12 @@ export const WarmPhase: FunctionComponent<Props> = ({
errors,
isShowingErrors,
}) => {
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
watch: [useRolloverPath],
const [formData] = useFormData({
watch: useRolloverPath,
});
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
return (
<div id="warmPhaseContent" aria-live="polite" role="region" aria-relevant="additions">
<>

View file

@ -125,9 +125,9 @@ export const SimulateTemplateFlyoutContent = ({
<EuiSpacer />
<FormDataProvider>
<FormDataProvider<Filters>>
{(formData) => {
return <SimulateTemplate template={template} filters={formData as Filters} />;
return <SimulateTemplate template={template} filters={formData} />;
}}
</FormDataProvider>
</Form>

View file

@ -54,9 +54,7 @@ export const DynamicMappingSection = () => (
<FormDataProvider pathsToWatch={['dynamicMapping.enabled', 'dynamicMapping.date_detection']}>
{(formData) => {
const {
'dynamicMapping.enabled': enabled,
// eslint-disable-next-line @typescript-eslint/naming-convention
'dynamicMapping.date_detection': dateDetection,
dynamicMapping: { enabled, date_detection: dateDetection },
} = formData;
if (enabled === undefined) {

View file

@ -155,7 +155,9 @@ export const SourceFieldSection = () => {
>
<FormDataProvider pathsToWatch={['sourceField.enabled']}>
{(formData) => {
const { 'sourceField.enabled': enabled } = formData;
const {
sourceField: { enabled },
} = formData;
if (enabled === undefined) {
return null;

View file

@ -53,7 +53,7 @@ export const AnalyzerParameterSelects = ({
useEffect(() => {
const subscription = subscribe((updateData) => {
const formData = updateData.data.raw;
const formData = updateData.data.internal;
const value = formData.sub ? formData.sub : formData.main;
onChange(value);
});

View file

@ -5,6 +5,7 @@
*/
import React, { useState } from 'react';
import { get } from 'lodash';
import {
EuiFlexGroup,
EuiFlexItem,
@ -193,7 +194,7 @@ export const EditFieldFormRow = React.memo(
return formFieldPath ? (
<FormDataProvider pathsToWatch={formFieldPath}>
{(formData) => {
setIsContentVisible(formData[formFieldPath]);
setIsContentVisible(get(formData, formFieldPath));
return renderContent();
}}
</FormDataProvider>

View file

@ -6,7 +6,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { NormalizedField, Field as FieldType } from '../../../../types';
import { NormalizedField, Field as FieldType, ComboBoxOption } from '../../../../types';
import { getFieldConfig } from '../../../../lib';
import { UseField, FormDataProvider, NumericField, Field } from '../../../../shared_imports';
import {
@ -48,9 +48,9 @@ export const NumericType = ({ field }: Props) => {
<>
<BasicParametersSection>
{/* scaling_factor */}
<FormDataProvider pathsToWatch="subType">
{(formData) =>
formData.subType === 'scaled_float' ? (
<FormDataProvider<{ subType?: ComboBoxOption[] }> pathsToWatch="subType">
{(formData) => {
return formData.subType?.[0]?.value === 'scaled_float' ? (
<EditFieldFormRow
title={PARAMETERS_DEFINITION.scaling_factor.title!}
description={PARAMETERS_DEFINITION.scaling_factor.description}
@ -62,8 +62,8 @@ export const NumericType = ({ field }: Props) => {
component={Field}
/>
</EditFieldFormRow>
) : null
}
) : null;
}}
</FormDataProvider>
<IndexParameter hasIndexOptions={false} />

View file

@ -5,7 +5,12 @@
*/
import React from 'react';
import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
import {
NormalizedField,
Field as FieldType,
ParameterName,
ComboBoxOption,
} from '../../../../types';
import { getFieldConfig } from '../../../../lib';
import {
StoreParameter,
@ -33,9 +38,9 @@ export const RangeType = ({ field }: Props) => {
<BasicParametersSection>
<IndexParameter hasIndexOptions={false} />
<FormDataProvider pathsToWatch="subType">
<FormDataProvider<{ subType?: ComboBoxOption[] }> pathsToWatch="subType">
{(formData) =>
formData.subType === 'date_range' ? (
formData.subType?.[0]?.value === 'date_range' ? (
<FormatParameter
defaultValue={field.source.format as string}
defaultToggleValue={getDefaultToggleValue('format', field.source)}
@ -46,9 +51,9 @@ export const RangeType = ({ field }: Props) => {
</BasicParametersSection>
<AdvancedParametersSection>
<FormDataProvider pathsToWatch="subType">
<FormDataProvider<{ subType?: ComboBoxOption[] }> pathsToWatch="subType">
{(formData) =>
formData.subType === 'date_range' ? (
formData.subType?.[0]?.value === 'date_range' ? (
<LocaleParameter defaultToggleValue={getDefaultToggleValue('locale', field.source)} />
) : null
}

View file

@ -18,7 +18,7 @@ export const StateProvider: React.FC = ({ children }) => {
configuration: {
defaultValue: {},
data: {
raw: {},
internal: {},
format: () => ({}),
},
validate: () => Promise.resolve(true),
@ -26,7 +26,7 @@ export const StateProvider: React.FC = ({ children }) => {
templates: {
defaultValue: {},
data: {
raw: {},
internal: {},
format: () => ({}),
},
validate: () => Promise.resolve(true),

View file

@ -176,7 +176,7 @@ export const reducer = (state: State, action: Action): State => {
configuration: {
...state.configuration,
data: {
raw: action.value.configuration,
internal: action.value.configuration,
format: () => action.value.configuration,
},
defaultValue: action.value.configuration,
@ -184,7 +184,7 @@ export const reducer = (state: State, action: Action): State => {
templates: {
...state.templates,
data: {
raw: action.value.templates,
internal: action.value.templates,
format: () => action.value.templates,
},
defaultValue: action.value.templates,
@ -217,7 +217,7 @@ export const reducer = (state: State, action: Action): State => {
isValid: true,
defaultValue: action.value,
data: {
raw: action.value,
internal: action.value,
format: () => action.value,
},
validate: async () => true,
@ -241,7 +241,7 @@ export const reducer = (state: State, action: Action): State => {
isValid: true,
defaultValue: action.value,
data: {
raw: action.value,
internal: action.value,
format: () => action.value,
},
validate: async () => true,

View file

@ -35,8 +35,8 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => {
const isFieldFormVisible = state.fieldForm !== undefined;
const emptyNameValue =
isFieldFormVisible &&
(state.fieldForm!.data.raw.name === undefined ||
state.fieldForm!.data.raw.name.trim() === '');
(state.fieldForm!.data.internal.name === undefined ||
state.fieldForm!.data.internal.name.trim() === '');
const bypassFieldFormValidation =
state.documentFields.status === 'creatingField' && emptyNameValue;

View file

@ -157,7 +157,7 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
getFormData,
} = form;
const [{ addMeta }] = useFormData({
const [{ addMeta }] = useFormData<{ addMeta: boolean }>({
form,
watch: 'addMeta',
});

View file

@ -6,7 +6,7 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import {
FIELD_TYPES,
UseField,
@ -44,8 +44,8 @@ export const Json: FunctionComponent = () => {
const form = useFormContext();
const [isAddToPathDisabled, setIsAddToPathDisabled] = useState<boolean>(false);
useEffect(() => {
const subscription = form.subscribe(({ data: { raw: rawData } }) => {
const hasTargetField = !!rawData[TARGET_FIELD_PATH];
const subscription = form.subscribe(({ data: { internal } }) => {
const hasTargetField = !!get(internal, TARGET_FIELD_PATH);
if (hasTargetField && !isAddToPathDisabled) {
setIsAddToPathDisabled(true);
form.getFields()[ADD_TO_ROOT_FIELD_PATH].setValue(false);

View file

@ -21,6 +21,7 @@ import { Document } from '../../types';
import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_tabs';
import { TestPipelineFlyoutForm } from './test_pipeline_flyout.container';
export interface Props {
onClose: () => void;
handleTestPipeline: (

View file

@ -21,7 +21,6 @@ import {
ValidationFuncArg,
FormHook,
Form,
useFormData,
} from '../../../../../../../shared_imports';
import { Document } from '../../../../types';
import { AddDocumentsAccordion } from './add_documents_accordion';
@ -149,16 +148,15 @@ export const DocumentsTab: FunctionComponent<Props> = ({
resetTestOutput,
}) => {
const { services } = useKibana();
const [, formatData] = useFormData({ form });
const { getFormData, reset } = form;
const onAddDocumentHandler = useCallback(
(document) => {
const { documents: existingDocuments = [] } = formatData();
const { documents: existingDocuments = [] } = getFormData();
form.reset({ defaultValue: { documents: [...existingDocuments, document] } });
reset({ defaultValue: { documents: [...existingDocuments, document] } });
},
[form, formatData]
[reset, getFormData]
);
const [showResetModal, setShowResetModal] = useState<boolean>(false);

View file

@ -54,7 +54,7 @@ export const AddComment = React.memo(
const fieldName = 'comment';
const { setFieldValue, reset, submit } = form;
const [{ comment }] = useFormData({ form, watch: [fieldName] });
const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] });
const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
setFieldValue,

View file

@ -87,10 +87,10 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
schema,
});
const { getFields, getFormData, submit } = form;
const [{ severity: formSeverity }] = (useFormData({
const [{ severity: formSeverity }] = useFormData<AboutStepRule>({
form,
watch: ['severity'],
}) as unknown) as [Partial<AboutStepRule>];
});
useEffect(() => {
const formSeverityValue = formSeverity?.value;

View file

@ -141,17 +141,15 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
'threshold.value': formThresholdValue,
'threshold.field': formThresholdField,
},
] = (useFormData({
] = useFormData<
DefineStepRule & {
'threshold.value': number | undefined;
'threshold.field': string[] | undefined;
}
>({
form,
watch: ['index', 'ruleType', 'queryBar', 'threshold.value', 'threshold.field', 'threatIndex'],
}) as unknown) as [
Partial<
DefineStepRule & {
'threshold.value': number | undefined;
'threshold.field': string[] | undefined;
}
>
];
});
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
const index = formIndex || initialState.index;
const threatIndex = formThreatIndex || initialState.threatIndex;

View file

@ -93,10 +93,10 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
schema,
});
const { getFields, getFormData, submit } = form;
const [{ throttle: formThrottle }] = (useFormData({
const [{ throttle: formThrottle }] = useFormData<ActionsStepRule>({
form,
watch: ['throttle'],
}) as unknown) as [Partial<ActionsStepRule>];
});
const throttle = formThrottle || initialState.throttle;
const handleSubmit = useCallback(