UNPKG

@atlaskit/form

Version:

A form allows people to input information.

194 lines (189 loc) 5.82 kB
import _extends from "@babel/runtime/helpers/extends"; import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createForm } from 'final-form'; import createDecorator from 'final-form-focus'; import set from 'lodash/set'; import forwardRefWithGeneric from '@atlaskit/ds-lib/forward-ref-with-generic'; import mergeRefs from '@atlaskit/ds-lib/merge-refs'; import { getFirstErrorField } from './utils'; /** * __Form context__ * * A form context creates a context for the field values and allows them to be accessed by the children. */ export const FormContext = /*#__PURE__*/createContext({ registerField: function () { return () => {}; }, getCurrentValue: () => undefined, subscribe: function () { return () => {}; } }); /** * __Is disabled context__ * * An is disabled context creates the context for when a value is disabled. */ export const IsDisabledContext = /*#__PURE__*/createContext(false); const FormBase = (props, ref) => { const { autocomplete, formProps: userProvidedFormProps, id, label, labelId, name, noValidate, onSubmit, testId, xcss } = props; const formRef = useRef(null); const onSubmitRef = useRef(onSubmit); onSubmitRef.current = onSubmit; const [form] = useState(() => { // Types here would break the existing API const finalForm = createForm({ onSubmit: (...args) => onSubmitRef.current(...args), destroyOnUnregister: true, initialValues: {}, mutators: { setDefaultValue: ([name, defaultValue], state) => { if (state.formState.initialValues) { const initialValues = state.formState.initialValues; const values = state.formState.values; const value = name && typeof defaultValue === 'function' ? defaultValue(initialValues[name]) : defaultValue; set(initialValues, name, value); set(values, name, value); } } } }); createDecorator(() => formRef.current ? Array.from(formRef.current.querySelectorAll('input')) : [], getFirstErrorField)(finalForm); return finalForm; }); const [state, setState] = useState({ dirty: false, submitting: false }); useEffect(() => { const unsubscribe = form.subscribe(({ dirty, submitting }) => { setState({ dirty, submitting }); }, { dirty: true, submitting: true }); return unsubscribe; }, [form]); const registerField = useCallback((name, defaultValue, subscriber, subscription, config) => { form.pauseValidation(); const unsubscribe = form.registerField(name, subscriber, subscription, config); form.mutators.setDefaultValue(name, defaultValue); form.resumeValidation(); return unsubscribe; }, [form]); const handleSubmit = e => { if (e) { e.preventDefault(); } form.submit(); }; const handleReset = initialValues => { form.reset(initialValues); }; const handleKeyDown = e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && formRef.current) { const submitButton = formRef.current.querySelector('button:not([type]), button[type="submit"], input[type="submit"]'); if (submitButton) { submitButton.click(); } e.preventDefault(); } }; const { isDisabled = false, children } = props; const { dirty, submitting } = state; /** * This method is needed in FormContext to use it on the field level * to check the current value of the field in case of the component re-mounting. */ const getCurrentValue = useCallback(name => { const formState = form.getState(); return (formState === null || formState === void 0 ? void 0 : formState.values[name]) || undefined; }, [form]); const FormContextValue = useMemo(() => { return { registerField, getCurrentValue, subscribe: form.subscribe }; }, [registerField, getCurrentValue, form.subscribe]); const conditionalFormProps = { autocomplete, className: xcss, id, 'aria-label': label, 'aria-labelledby': labelId, name, noValidate, 'data-testid': testId }; // Abstracting so we can use the same for both rendering patterns const formProps = { onKeyDown: handleKeyDown, onSubmit: handleSubmit, ref: ref ? mergeRefs([ref, formRef]) : formRef }; // We don't want to add undefined values to the component Object.keys(conditionalFormProps).forEach(attr => { if (conditionalFormProps[attr] !== undefined) { formProps[attr] = conditionalFormProps[attr]; } }); const childrenContent = (() => { if (typeof children === 'function') { const result = children.length > 0 ? children({ formProps, dirty, reset: handleReset, submitting, disabled: isDisabled, getState: () => form.getState(), getValues: () => form.getState().values, setFieldValue: form.change, resetFieldState: form.resetFieldState }) : children(); return result === undefined ? null : result; } else { return /*#__PURE__*/React.createElement("form", _extends({}, formProps, userProvidedFormProps), children); } })(); return /*#__PURE__*/React.createElement(FormContext.Provider, { value: FormContextValue }, /*#__PURE__*/React.createElement(IsDisabledContext.Provider, { value: isDisabled }, childrenContent)); }; /** * __Form__ * * A form allows users to input information. * * - [Examples](https://atlassian.design/components/form/examples) * - [Code](https://atlassian.design/components/form/code) * - [Usage](https://atlassian.design/components/form/usage) */ const Form = forwardRefWithGeneric(FormBase); export default Form;