diff --git a/pkg/webui/components/form/context.js b/pkg/webui/components/form/context.js
deleted file mode 100644
index 86ca8e4ae1..0000000000
--- a/pkg/webui/components/form/context.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
-//
-// Licensed 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 from 'react'
-
-export default React.createContext()
diff --git a/pkg/webui/components/form/field/index.js b/pkg/webui/components/form/field/index.js
index d10566fefb..ceaf099276 100644
--- a/pkg/webui/components/form/field/index.js
+++ b/pkg/webui/components/form/field/index.js
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React, { useCallback, useContext } from 'react'
+import React, { useCallback } from 'react'
import classnames from 'classnames'
import { useField } from 'formik'
import { isPlainObject } from 'lodash'
@@ -22,7 +22,7 @@ import Message from '@ttn-lw/lib/components/message'
import from from '@ttn-lw/lib/from'
import PropTypes from '@ttn-lw/lib/prop-types'
-import FormContext from '../context'
+import { useFormContext } from '..'
import Tooltip from './tooltip'
import FieldError from './error'
@@ -89,8 +89,16 @@ const FormField = props => {
onChange,
onBlur,
} = props
- const { disabled: formDisabled, validateOnBlur } = useContext(FormContext)
- const [{ value: encodedValue }, { touched, error = false }, { setTouched, setValue }] = useField({
+
+ const {
+ disabled: formDisabled,
+ validateOnBlur,
+ setFieldValue,
+ setFieldTouched,
+ } = useFormContext()
+
+ // Initialize field, which also takes care of registering fields in formik's internal registry.
+ const [{ value: encodedValue }, { touched, error = false }] = useField({
name,
validate,
})
@@ -111,27 +119,27 @@ const FormField = props => {
}
}
- await setValue(newValue)
+ await setFieldValue(name, newValue)
if (enforceValidation) {
- setTouched(true, true)
+ setFieldTouched(name, true, true)
}
onChange(isSyntheticEvent ? value : encode(value))
},
- [setTouched, setValue, onChange, encode],
+ [encode, name, onChange, setFieldTouched, setFieldValue],
)
const handleBlur = useCallback(
event => {
if (validateOnBlur) {
const value = extractValue(event)
- setTouched(!isValueEmpty(value))
+ setFieldTouched(name, !isValueEmpty(value))
}
onBlur(event)
},
- [validateOnBlur, onBlur, setTouched],
+ [validateOnBlur, onBlur, setFieldTouched, name],
)
const value = decode(encodedValue)
@@ -142,7 +150,6 @@ const FormField = props => {
const showWarning = !Boolean(error) && Boolean(warning)
const showDescription = !showError && !showWarning && Boolean(description)
const tooltipIcon = hasTooltip ? : null
-
const describedBy = showError
? `${name}-field-error`
: showWarning
diff --git a/pkg/webui/components/form/field/info.js b/pkg/webui/components/form/field/info.js
index 4a2586f9b2..964648e06a 100644
--- a/pkg/webui/components/form/field/info.js
+++ b/pkg/webui/components/form/field/info.js
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React, { useContext } from 'react'
+import React from 'react'
import classnames from 'classnames'
import Message from '@ttn-lw/lib/components/message'
@@ -20,7 +20,7 @@ import Message from '@ttn-lw/lib/components/message'
import PropTypes from '@ttn-lw/lib/prop-types'
import from from '@ttn-lw/lib/from'
-import FormContext from '../context'
+import { useFormContext } from '..'
import Tooltip from './tooltip'
@@ -28,7 +28,7 @@ import style from './field.styl'
const InfoField = props => {
const { children, className, title, disabled: fieldDisabled, tooltipId } = props
- const { disabled: formDisabled } = useContext(FormContext)
+ const { disabled: formDisabled } = useFormContext()
const disabled = formDisabled || fieldDisabled
const cls = classnames(className, style.field, from(style, { disabled }))
diff --git a/pkg/webui/components/form/index.js b/pkg/webui/components/form/index.js
index 280fff7006..c1022329c9 100644
--- a/pkg/webui/components/form/index.js
+++ b/pkg/webui/components/form/index.js
@@ -12,11 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-/* eslint-disable react/sort-prop-types */
import React, { useCallback, useEffect } from 'react'
-import { Formik, yupToFormErrors, useFormikContext, validateYupSchema } from 'formik'
+import {
+ yupToFormErrors,
+ useFormikContext,
+ validateYupSchema,
+ useFormik,
+ FormikProvider,
+} from 'formik'
import scrollIntoView from 'scroll-into-view-if-needed'
import { defineMessages } from 'react-intl'
+import { isPlainObject } from 'lodash'
import Notification from '@ttn-lw/components/notification'
import ErrorNotification from '@ttn-lw/components/error-notification'
@@ -24,7 +30,6 @@ import ErrorNotification from '@ttn-lw/components/error-notification'
import PropTypes from '@ttn-lw/lib/prop-types'
import { ingestError } from '@ttn-lw/lib/errors/utils'
-import FormContext from './context'
import FormField from './field'
import FormInfoField from './field/info'
import FormSubmit from './submit'
@@ -36,82 +41,6 @@ const m = defineMessages({
submitFailed: 'Submit failed',
})
-const InnerForm = props => {
- const {
- formError,
- isSubmitting,
- isValid,
- className,
- children,
- formErrorTitle,
- formInfo,
- formInfoTitle,
- handleSubmit,
- id,
- ...rest
- } = props
- const notificationRef = React.useRef()
-
- useEffect(() => {
- // Scroll form notification into view if needed.
- if (formError) {
- scrollIntoView(notificationRef.current, { behavior: 'smooth' })
- notificationRef.current.focus({ preventScroll: true })
- }
-
- // Scroll invalid fields into view if needed and focus them.
- if (!isSubmitting && !isValid) {
- const firstErrorNode = document.querySelectorAll('[data-needs-focus="true"]')[0]
- if (firstErrorNode) {
- scrollIntoView(firstErrorNode, { behavior: 'smooth' })
- firstErrorNode.querySelector('input,textarea,canvas,video').focus({ preventScroll: true })
- }
- }
- }, [formError, isSubmitting, isValid])
-
- return (
-
- )
-}
-InnerForm.propTypes = {
- children: PropTypes.node.isRequired,
- className: PropTypes.string,
- id: PropTypes.string,
- formError: PropTypes.error,
- formErrorTitle: PropTypes.message,
- formInfo: PropTypes.message,
- formInfoTitle: PropTypes.message,
- handleSubmit: PropTypes.func.isRequired,
- isSubmitting: PropTypes.bool.isRequired,
- isValid: PropTypes.bool.isRequired,
-}
-
-InnerForm.defaultProps = {
- className: undefined,
- id: undefined,
- formInfo: undefined,
- formInfoTitle: undefined,
- formError: undefined,
- formErrorTitle: m.submitFailed,
-}
-
const Form = props => {
const {
children,
@@ -135,22 +64,10 @@ const Form = props => {
validationSchema,
} = props
- const handleSubmit = useCallback(
- async (...args) => {
- try {
- return await onSubmit(...args)
- } catch (error) {
- // Make sure all unhandled exceptions during submit are ingested.
- ingestError(error, { ingestedBy: 'FormSubmit' })
-
- throw error
- }
- },
- [onSubmit],
- )
+ const notificationRef = React.useRef()
// Recreate the validation hook to allow passing down validation contexts.
- const validate = useEffect(
+ const validate = useCallback(
values => {
if (!validationSchema) {
return {}
@@ -190,35 +107,81 @@ const Form = props => {
[validationSchema, validateSync, validationContext, error],
)
+ const handleSubmit = useCallback(
+ async (...args) => {
+ try {
+ return await onSubmit(...args)
+ } catch (error) {
+ // Make sure all unhandled exceptions during submit are ingested.
+ ingestError(error, { ingestedBy: 'FormSubmit' })
+
+ throw error
+ }
+ },
+ [onSubmit],
+ )
+
+ // Initialize formik and get the formik context to provide to form children.
+ const formik = useFormik({
+ initialValues,
+ validate,
+ onSubmit: handleSubmit,
+ onReset,
+ validateOnMount,
+ validateOnBlur,
+ validateSync,
+ validateOnChange,
+ enableReinitialize,
+ })
+
+ const {
+ isSubmitting,
+ isValid,
+ handleSubmit: handleFormikSubmit,
+ handleReset: handleFormikReset,
+ } = formik
+
+ // Connect the ref with the formik context to ensure compatibility with older form components.
+ // NOTE: New components should not use the ref, but use the form context directly.
+ // TODO: Remove this once all forms have been refactored to use context.
+ if (isPlainObject(formikRef) && 'current' in formikRef) {
+ formikRef.current = formik
+ }
+
+ useEffect(() => {
+ // Scroll form notification into view if needed.
+ if (error) {
+ scrollIntoView(notificationRef.current, { behavior: 'smooth' })
+ notificationRef.current.focus({ preventScroll: true })
+ }
+
+ // Scroll invalid fields into view if needed and focus them.
+ if (!isSubmitting && !isValid) {
+ const firstErrorNode = document.querySelectorAll('[data-needs-focus="true"]')[0]
+ if (firstErrorNode) {
+ scrollIntoView(firstErrorNode, { behavior: 'smooth' })
+ firstErrorNode.querySelector('input,textarea,canvas,video').focus({ preventScroll: true })
+ }
+ }
+ }, [error, isSubmitting, isValid])
+
return (
-
- {({ handleSubmit, ...restFormikProps }) => (
-
- {children}
-
- )}
-
+
+
)
}
@@ -229,10 +192,10 @@ Form.propTypes = {
enableReinitialize: PropTypes.bool,
error: PropTypes.error,
errorTitle: PropTypes.message,
- info: PropTypes.message,
- infoTitle: PropTypes.message,
formikRef: PropTypes.shape({ current: PropTypes.shape({}) }),
id: PropTypes.string,
+ info: PropTypes.message,
+ infoTitle: PropTypes.message,
initialValues: PropTypes.shape({}),
onReset: PropTypes.func,
onSubmit: PropTypes.func,
diff --git a/pkg/webui/components/form/section/index.js b/pkg/webui/components/form/section/index.js
index ba04122be4..879aabe88d 100644
--- a/pkg/webui/components/form/section/index.js
+++ b/pkg/webui/components/form/section/index.js
@@ -16,13 +16,14 @@ import React from 'react'
import classnames from 'classnames'
import { defineMessages, useIntl } from 'react-intl'
-import { useFormContext } from '@ttn-lw/components/form'
import Icon from '@ttn-lw/components/icon'
import Message from '@ttn-lw/lib/components/message'
import PropTypes from '@ttn-lw/lib/prop-types'
+import { useFormContext } from '..'
+
import style from './section.styl'
const m = defineMessages({
diff --git a/pkg/webui/components/form/submit/index.js b/pkg/webui/components/form/submit/index.js
index 0e9bbb0d2f..ed8bf55efe 100644
--- a/pkg/webui/components/form/submit/index.js
+++ b/pkg/webui/components/form/submit/index.js
@@ -12,18 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React, { useContext } from 'react'
+import React from 'react'
import PropTypes from '@ttn-lw/lib/prop-types'
-import FormContext from '../context'
+import { useFormContext } from '..'
const FormSubmit = props => {
const { component: Component, disabled, ...rest } = props
- const formContext = useContext(FormContext)
+ const formContext = useFormContext()
const submitProps = {
- isValid: context.isValid,
+ isValid: formContext.isValid,
isSubmitting: formContext.isSubmitting,
isValidating: formContext.isValidating,
submitCount: formContext.submitCount,