Skip to content

Commit

Permalink
[7.x] Form lib enhancements (#78588) (#79494)
Browse files Browse the repository at this point in the history
Co-authored-by: Patryk Kopycinski <[email protected]>
Co-authored-by: Elastic Machine <[email protected]>
Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
4 people authored Oct 6, 2020
1 parent 9cb4774 commit d5445d7
Show file tree
Hide file tree
Showing 36 changed files with 291 additions and 219 deletions.
2 changes: 2 additions & 0 deletions src/plugins/es_ui_shared/static/forms/components/field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SelectField,
SuperSelectField,
ToggleField,
JsonEditorField,
} from './fields';

const mapTypeToFieldComponent: { [key: string]: ComponentType<any> } = {
Expand All @@ -52,6 +53,7 @@ const mapTypeToFieldComponent: { [key: string]: ComponentType<any> } = {
[FIELD_TYPES.SELECT]: SelectField,
[FIELD_TYPES.SUPER_SELECT]: SuperSelectField,
[FIELD_TYPES.TOGGLE]: ToggleField,
[FIELD_TYPES.JSON]: JsonEditorField,
};

export const Field = (props: Props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../public';
import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib';

interface Props {
field: FieldHook;
field: FieldHook<any, string>;
euiCodeEditorProps?: { [key: string]: any };
[key: string]: any;
}
Expand All @@ -44,7 +44,7 @@ export const JsonEditorField = ({ field, ...rest }: Props) => {
<JsonEditor
label={label}
helpText={helpText}
value={value as string}
value={value}
onUpdate={onJsonUpdate}
error={errorMessage}
{...rest}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,8 @@ interface Props {
path: string;
initialNumberOfItems?: number;
readDefaultValueOnForm?: boolean;
validations?: FieldConfig<any, ArrayItem[]>['validations'];
children: (args: {
items: ArrayItem[];
error: string | null;
addItem: () => void;
removeItem: (id: number) => void;
moveItem: (sourceIdx: number, destinationIdx: number) => void;
form: FormHook;
}) => JSX.Element;
validations?: FieldConfig<ArrayItem[]>['validations'];
children: (formFieldArray: FormArrayField) => JSX.Element;
}

export interface ArrayItem {
Expand All @@ -45,6 +38,15 @@ export interface ArrayItem {
isNew: boolean;
}

export interface FormArrayField {
items: ArrayItem[];
error: string | null;
addItem: () => void;
removeItem: (id: number) => void;
moveItem: (sourceIdx: number, destinationIdx: number) => void;
form: FormHook;
}

/**
* Use UseArray to dynamically add fields to your form.
*
Expand All @@ -71,7 +73,7 @@ export const UseArray = ({
const uniqueId = useRef(0);

const form = useFormContext();
const { getFieldDefaultValue } = form;
const { __getFieldDefaultValue } = form;

const getNewItemAtIndex = useCallback(
(index: number): ArrayItem => ({
Expand All @@ -84,7 +86,7 @@ export const UseArray = ({

const fieldDefaultValue = useMemo<ArrayItem[]>(() => {
const defaultValues = readDefaultValueOnForm
? (getFieldDefaultValue(path) as any[])
? (__getFieldDefaultValue(path) as any[])
: undefined;

const getInitialItemsFromValues = (values: any[]): ArrayItem[] =>
Expand All @@ -97,17 +99,23 @@ export const UseArray = ({
return defaultValues
? getInitialItemsFromValues(defaultValues)
: new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i));
}, [path, initialNumberOfItems, readDefaultValueOnForm, getFieldDefaultValue, getNewItemAtIndex]);
}, [
path,
initialNumberOfItems,
readDefaultValueOnForm,
__getFieldDefaultValue,
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.
// Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle.
const fieldConfigBase: FieldConfig<any, ArrayItem[]> & InternalFieldConfig<ArrayItem[]> = {
const fieldConfigBase: FieldConfig<ArrayItem[]> & InternalFieldConfig<ArrayItem[]> = {
defaultValue: fieldDefaultValue,
errorDisplayDelay: 0,
valueChangeDebounceTime: 0,
isIncludedInOutput: false,
};

const fieldConfig: FieldConfig<any, ArrayItem[]> & InternalFieldConfig<ArrayItem[]> = validations
const fieldConfig: FieldConfig<ArrayItem[]> & InternalFieldConfig<ArrayItem[]> = validations
? { validations, ...fieldConfigBase }
: fieldConfigBase;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@

import React, { FunctionComponent } from 'react';

import { FieldHook, FieldConfig } from '../types';
import { FieldHook, FieldConfig, FormData } from '../types';
import { useField } from '../hooks';
import { useFormContext } from '../form_context';

export interface Props<T> {
export interface Props<T, FormType = FormData, I = T> {
path: string;
config?: FieldConfig<any, T>;
config?: FieldConfig<T, FormType, I>;
defaultValue?: T;
component?: FunctionComponent<any> | 'input';
component?: FunctionComponent<any>;
componentProps?: Record<string, any>;
readDefaultValueOnForm?: boolean;
onChange?: (value: T) => void;
children?: (field: FieldHook<T>) => JSX.Element;
onChange?: (value: I) => void;
children?: (field: FieldHook<T, I>) => JSX.Element;
[key: string]: any;
}

function UseFieldComp<T = unknown>(props: Props<T>) {
function UseFieldComp<T = unknown, FormType = FormData, I = T>(props: Props<T, FormType, I>) {
const {
path,
config,
Expand All @@ -48,18 +48,16 @@ function UseFieldComp<T = unknown>(props: Props<T>) {
...rest
} = props;

const form = useFormContext();
const form = useFormContext<FormType>();
const ComponentToRender = component ?? 'input';
// For backward compatibility we merge the "componentProps" prop into the "rest"
const propsToForward =
componentProps !== undefined ? { ...componentProps, ...rest } : { ...rest };
const propsToForward = { ...componentProps, ...rest };

const fieldConfig: FieldConfig<any, T> & { initialValue?: T } =
const fieldConfig: FieldConfig<T, FormType, I> & { initialValue?: T } =
config !== undefined
? { ...config }
: ({
...form.__readFieldConfigFromSchema(path),
} as Partial<FieldConfig<any, T>>);
} as Partial<FieldConfig<T, FormType, I>>);

if (defaultValue !== undefined) {
// update the form "defaultValue" ref object so when/if we reset the form we can go back to this value
Expand All @@ -70,21 +68,12 @@ function UseFieldComp<T = unknown>(props: Props<T>) {
} else {
if (readDefaultValueOnForm) {
// Read the field initial value from the "defaultValue" object passed to the form
fieldConfig.initialValue = (form.getFieldDefaultValue(path) as T) ?? fieldConfig.defaultValue;
fieldConfig.initialValue =
(form.__getFieldDefaultValue(path) as T) ?? fieldConfig.defaultValue;
}
}

if (!fieldConfig.path) {
(fieldConfig.path as any) = path;
} else {
if (fieldConfig.path !== path) {
throw new Error(
`Field path mismatch. Got "${path}" but field config has "${fieldConfig.path}".`
);
}
}

const field = useField<T>(form, path, fieldConfig, onChange);
const field = useField<T, FormType, I>(form, path, fieldConfig, onChange);

// Children prevails over anything else provided.
if (children) {
Expand All @@ -111,9 +100,13 @@ export const UseField = React.memo(UseFieldComp) as typeof UseFieldComp;
* Get a <UseField /> component providing some common props for all instances.
* @param partialProps Partial props to apply to all <UseField /> instances
*/
export function getUseField<T1 = unknown>(partialProps: Partial<Props<T1>>) {
return function <T2 = T1>(props: Partial<Props<T2>>) {
const componentProps = { ...partialProps, ...props } as Props<T2>;
return <UseField<T2> {...componentProps} />;
export function getUseField<T1 = unknown, FormType1 = FormData, I1 = T1>(
partialProps: Partial<Props<T1, FormType1, I1>>
) {
return function <T2 = T1, FormType2 = FormType1, I2 = I1>(
props: Partial<Props<T2, FormType2, I2>>
) {
const componentProps = { ...partialProps, ...props } as Props<T2, FormType2, I2>;
return <UseField<T2, FormType2, I2> {...componentProps} />;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ import React from 'react';
import { UseField, Props as UseFieldProps } from './use_field';
import { FieldHook } from '../types';

type FieldsArray = Array<{ id: string } & Omit<UseFieldProps<unknown>, 'children'>>;
type FieldsArray = Array<{ id: string } & Omit<UseFieldProps<unknown, {}, unknown>, 'children'>>;

interface Props {
fields: { [key: string]: Exclude<UseFieldProps<unknown>, 'children'> };
children: (fields: { [key: string]: FieldHook }) => JSX.Element;
interface Props<T> {
fields: { [K in keyof T]: Exclude<UseFieldProps<T[K]>, 'children'> };
children: (fields: { [K in keyof T]: FieldHook<T[K]> }) => JSX.Element;
}

export const UseMultiFields = ({ fields, children }: Props) => {
export function UseMultiFields<T = { [key: string]: unknown }>({ fields, children }: Props<T>) {
const fieldsArray = Object.entries(fields).reduce(
(acc, [fieldId, field]) => [...acc, { id: fieldId, ...field }],
(acc, [fieldId, field]) => [...acc, { id: fieldId, ...(field as FieldHook) }],
[] as FieldsArray
);

const hookFields: { [key: string]: FieldHook } = {};
const hookFields: { [K in keyof T]: FieldHook<any> } = {} as any;

const renderField = (index: number) => {
const { id } = fieldsArray[index];
return (
<UseField {...fields[id]}>
<UseField {...fields[id as keyof T]}>
{(field) => {
hookFields[id] = field;
hookFields[id as keyof T] = field;
return index === fieldsArray.length - 1 ? children(hookFields) : renderField(index + 1);
}}
</UseField>
Expand All @@ -54,4 +54,4 @@ export const UseMultiFields = ({ fields, children }: Props) => {
}

return renderField(0);
};
}
10 changes: 7 additions & 3 deletions src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export const FIELD_TYPES = {
SELECT: 'select',
SUPER_SELECT: 'superSelect',
MULTI_SELECT: 'multiSelect',
JSON: 'json',
};

// Validation types
export const VALIDATION_TYPES = {
FIELD: 'field', // Default validation error (on the field value)
ASYNC: 'async', // Returned from asynchronous validations
ARRAY_ITEM: 'arrayItem', // If the field value is an Array, this error would be returned if an _item_ of the array is invalid
/** Default validation error (on the field value) */
FIELD: 'field',
/** Returned from asynchronous validations */
ASYNC: 'async',
/** If the field value is an Array, this error type would be returned if an _item_ of the array is invalid */
ARRAY_ITEM: 'arrayItem',
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import React, { createContext, useContext, useMemo } from 'react';
import { FormData, FormHook } from './types';
import { Subject } from './lib';

export interface Context<T extends FormData = FormData> {
getFormData$: () => Subject<FormData>;
export interface Context<T extends FormData = FormData, I = T> {
getFormData$: () => Subject<I>;
getFormData: FormHook<T>['getFormData'];
}

Expand Down
Loading

0 comments on commit d5445d7

Please sign in to comment.