Skip to content

Commit

Permalink
Hook form lib (#41658) (#45122)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored Sep 9, 2019
1 parent 2db4beb commit 617d4fb
Show file tree
Hide file tree
Showing 15 changed files with 1,341 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React, { ReactNode } from 'react';
import { EuiForm } from '@elastic/eui';

import { FormProvider } from '../form_context';
import { FormHook } from '../types';

interface Props {
form: FormHook<any>;
FormWrapper?: (props: any) => JSX.Element;
children: ReactNode | ReactNode[];
className: string;
}

const DefaultFormWrapper = (props: any) => {
return <EuiForm {...props} />;
};

export const Form = ({ form, FormWrapper = DefaultFormWrapper, ...rest }: Props) => (
<FormProvider form={form}>
<FormWrapper {...rest} />
</FormProvider>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { useState, useEffect, useRef } from 'react';

import { FormData } from '../types';
import { Subscription } from '../lib';
import { useFormContext } from '../form_context';

interface Props {
children: (formData: FormData) => JSX.Element | null;
pathsToWatch?: string | string[];
}

export const FormDataProvider = ({ children, pathsToWatch }: Props) => {
const [formData, setFormData] = useState<FormData>({});
const previousState = useRef<FormData>({});
const subscription = useRef<Subscription | null>(null);
const form = useFormContext();

useEffect(() => {
subscription.current = form.__formData$.current.subscribe(data => {
// To avoid re-rendering the children for updates on the form data
// that we are **not** interested in, we can specify one or multiple path(s)
// to watch.
if (pathsToWatch) {
const valuesToWatchArray = Array.isArray(pathsToWatch)
? (pathsToWatch as string[])
: ([pathsToWatch] as string[]);
if (valuesToWatchArray.some(value => previousState.current[value] !== data[value])) {
previousState.current = data;
setFormData(data);
}
} else {
setFormData(data);
}
});

return () => {
subscription.current!.unsubscribe();
};
}, [pathsToWatch]);

return children(formData);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export * from './form';
export * from './use_field';
export * from './use_array';
export * from './form_data_provider';
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { useState, useRef } from 'react';

import { useFormContext } from '../form_context';

interface Props {
path: string;
initialNumberOfItems?: number;
children: (args: {
items: ArrayItem[];
addItem: () => void;
removeItem: (id: number) => void;
}) => JSX.Element;
}

export interface ArrayItem {
id: number;
path: string;
isNew: boolean;
}

/**
* Use UseArray to dynamically add fields to your form.
*
* example:
* If your form data looks like this:
*
* {
* users: []
* }
*
* and you want to be able to add user objects ({ name: 'john', lastName. 'snow' }) inside
* the "users" array, you would use UseArray to render rows of user objects with 2 fields in each of them ("name" and "lastName")
*
* Look at the README.md for some examples.
*/
export const UseArray = ({ path, initialNumberOfItems, children }: Props) => {
const form = useFormContext();
const defaultValues = form.getFieldDefaultValue(path) as any[];
const uniqueId = useRef(0);

const getInitialItemsFromValues = (values: any[]): ArrayItem[] =>
values.map((_, index) => ({
id: uniqueId.current++,
path: `${path}[${index}]`,
isNew: false,
}));

const getNewItemAtIndex = (index: number): ArrayItem => ({
id: uniqueId.current++,
path: `${path}[${index}]`,
isNew: true,
});

const initialState = defaultValues
? getInitialItemsFromValues(defaultValues)
: new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i));

const [items, setItems] = useState<ArrayItem[]>(initialState);

const updatePaths = (_rows: ArrayItem[]) =>
_rows.map(
(row, index) =>
({
...row,
path: `${path}[${index}]`,
} as ArrayItem)
);

const addItem = () => {
setItems(previousItems => {
const itemIndex = previousItems.length;
return [...previousItems, getNewItemAtIndex(itemIndex)];
});
};

const removeItem = (id: number) => {
setItems(previousItems => {
const updatedItems = previousItems.filter(item => item.id !== id);
return updatePaths(updatedItems);
});
};

return children({ items, addItem, removeItem });
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React, { useEffect, FunctionComponent } from 'react';

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

interface Props {
path: string;
config?: FieldConfig<any>;
defaultValue?: unknown;
component?: FunctionComponent<any> | 'input';
componentProps?: Record<string, any>;
children?: (field: FieldHook) => JSX.Element;
}

export const UseField = ({
path,
config,
defaultValue,
component = 'input',
componentProps = {},
children,
}: Props) => {
const form = useFormContext();

if (typeof defaultValue === 'undefined') {
defaultValue = form.getFieldDefaultValue(path);
}

if (!config) {
config = form.__readFieldConfigFromSchema(path);
}

// Don't modify the config object
const configCopy =
typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config };

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

const field = useField(form, path, configCopy);

// Remove field from form when it is unmounted or if its path changes
useEffect(() => {
return () => {
form.__removeField(path);
};
}, [path]);

// Children prevails over anything else provided.
if (children) {
return children!(field);
}

if (component === 'input') {
return (
<input
type={field.type}
onChange={field.onChange}
value={field.value as string}
{...componentProps}
/>
);
}

return component({ field, ...componentProps });
};
36 changes: 36 additions & 0 deletions src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// Field types
export const FIELD_TYPES = {
TEXT: 'text',
NUMBER: 'number',
TOGGLE: 'toggle',
CHECKBOX: 'checkbox',
COMBO_BOX: 'comboBox',
SELECT: 'select',
MULTI_SELECT: 'multiSelect',
};

// 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
};
Loading

0 comments on commit 617d4fb

Please sign in to comment.