-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[Mappings editor] Core #47335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Mappings editor] Core #47335
Changes from all commits
bcf377a
f279201
bb98a2e
9c4cee9
8576632
f220709
27ff056
e453dea
eefe40b
a9d5a3e
b063a8d
4066ae7
7a36eb4
c3c9b10
d4caf4e
8c51b76
bf94053
b2af545
a65dd77
11b7987
80c64d3
344344a
6f91b24
50f153f
ad8f739
ea546ee
08cb961
eb5a867
b9fed7e
6afa76f
af93fea
67b3a11
f716805
88c0b17
ccda3f3
3c4c425
2747f18
a17b470
d885bfe
ee8419f
3186bf2
5a8ce4b
f4d4e7f
1128dbe
e4ee1bd
5a28df1
293d9b0
22f54a3
99ebcf5
183c6be
a48b481
ed11862
a855468
2302d94
6bb5baa
5ed3291
44449c9
d53d722
4e5e37e
090c82a
a9aea0a
21d6339
c58edc1
13dd203
8e631ac
3dcb609
a36a3a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,11 +17,11 @@ | |
| * under the License. | ||
| */ | ||
|
|
||
| import { useState, useRef } from 'react'; | ||
| import { useState, useRef, useEffect } from 'react'; | ||
| import { get } from 'lodash'; | ||
|
|
||
| import { FormHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; | ||
| import { mapFormFields, flattenObject, unflattenObject, Subject } from '../lib'; | ||
| import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; | ||
| import { mapFormFields, flattenObject, unflattenObject, Subject, Subscription } from '../lib'; | ||
|
|
||
| const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; | ||
| const DEFAULT_OPTIONS = { | ||
|
|
@@ -47,10 +47,11 @@ export function useForm<T extends object = FormData>( | |
| const formOptions = { ...DEFAULT_OPTIONS, ...options }; | ||
| const defaultValueDeserialized = | ||
| Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue); | ||
| const [isSubmitted, setSubmitted] = useState(false); | ||
| const [isSubmitted, setIsSubmitted] = useState(false); | ||
| const [isSubmitting, setSubmitting] = useState(false); | ||
| const [isValid, setIsValid] = useState(true); | ||
| const [isValid, setIsValid] = useState<boolean | undefined>(undefined); | ||
| const fieldsRefs = useRef<FieldsMap>({}); | ||
| const formUpdateSubscribers = useRef<Subscription[]>([]); | ||
|
|
||
| // 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 | ||
|
|
@@ -59,6 +60,13 @@ export function useForm<T extends object = FormData>( | |
| // and updating its state to trigger the necessary view render. | ||
| const formData$ = useRef<Subject<T>>(new Subject<T>(flattenObject(defaultValue) as T)); | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| formUpdateSubscribers.current.forEach(subscription => subscription.unsubscribe()); | ||
| formUpdateSubscribers.current = []; | ||
| }; | ||
| }, []); | ||
|
|
||
| // -- HELPERS | ||
| // ---------------------------------- | ||
| const fieldsToArray = () => Object.values(fieldsRefs.current); | ||
|
|
@@ -78,6 +86,12 @@ export function useForm<T extends object = FormData>( | |
| return fields; | ||
| }; | ||
|
|
||
| const updateFormDataAt: FormHook<T>['__updateFormDataAt'] = (path, value) => { | ||
| const currentFormData = formData$.current.value; | ||
| formData$.current.next({ ...currentFormData, [path]: value }); | ||
| return formData$.current.value; | ||
| }; | ||
|
|
||
| // -- API | ||
| // ---------------------------------- | ||
| const getFormData: FormHook<T>['getFormData'] = (getDataOptions = { unflatten: true }) => | ||
|
|
@@ -93,34 +107,52 @@ export function useForm<T extends object = FormData>( | |
| {} as T | ||
| ); | ||
|
|
||
| const updateFormDataAt: FormHook<T>['__updateFormDataAt'] = (path, value) => { | ||
| const currentFormData = formData$.current.value; | ||
| formData$.current.next({ ...currentFormData, [path]: value }); | ||
| return formData$.current.value; | ||
| const isFieldValid = (field: FieldHook) => | ||
| field.getErrorsMessages() === null && !field.isValidating; | ||
|
|
||
| const updateFormValidity = () => { | ||
| const fieldsArray = fieldsToArray(); | ||
| const areAllFieldsValidated = fieldsArray.every(field => field.isValidated); | ||
|
|
||
| if (!areAllFieldsValidated) { | ||
| // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" | ||
| return; | ||
| } | ||
|
|
||
| const isFormValid = fieldsArray.every(isFieldValid); | ||
|
|
||
| setIsValid(isFormValid); | ||
| return isFormValid; | ||
| }; | ||
|
|
||
| /** | ||
| * When a field value changes, validateFields() is called with the field name + any other fields | ||
| * declared in the "fieldsToValidateOnChange" (see the field config). | ||
| * | ||
| * When this method is called _without_ providing any fieldNames, we only need to validate fields that are pristine | ||
| * as the fields that are dirty have already been validated when their value changed. | ||
| */ | ||
| const validateFields: FormHook<T>['__validateFields'] = async fieldNames => { | ||
| const fieldsToValidate = fieldNames | ||
| ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined) | ||
| : fieldsToArray().filter(field => field.isPristine); // only validate fields that haven't been changed | ||
| .map(name => fieldsRefs.current[name]) | ||
| .filter(field => field !== undefined); | ||
|
|
||
| const formData = getFormData({ unflatten: false }); | ||
| if (fieldsToValidate.length === 0) { | ||
| // Nothing to validate | ||
| return true; | ||
| } | ||
|
|
||
| const formData = getFormData({ unflatten: false }); | ||
| await Promise.all(fieldsToValidate.map(field => field.validate({ formData }))); | ||
| updateFormValidity(); | ||
|
|
||
| const isFormValid = fieldsToArray().every( | ||
| field => field.getErrorsMessages() === null && !field.isValidating | ||
| ); | ||
| setIsValid(isFormValid); | ||
| return fieldsToValidate.every(isFieldValid); | ||
| }; | ||
|
|
||
| return isFormValid; | ||
| const validateAllFields = async (): Promise<boolean> => { | ||
| const fieldsToValidate = fieldsToArray().filter(field => !field.isValidated); | ||
|
|
||
| if (fieldsToValidate.length === 0) { | ||
| // Nothing left to validate, all fields are already validated. | ||
| return isValid!; | ||
| } | ||
|
|
||
| await validateFields(fieldsToValidate.map(field => field.path)); | ||
|
|
||
| return updateFormValidity()!; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for adding the One thing looks strange to me here. Then on line 142, fieldsToValidate.every(isFieldValid)This code looks suspiciously like it's checking that every field is valid. Yet Ideally, it seems like there should be a single place where the form validity state is set, and then any place we want to surface that state we just return
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good catch, I will remove the one in
This is the case, the only place where the form validity is set is inside
Not sure why we would prefer a ref than a state. This is probably the most important state of the form 😊 |
||
| }; | ||
|
|
||
| const addField: FormHook<T>['__addField'] = field => { | ||
|
|
@@ -170,32 +202,59 @@ export function useForm<T extends object = FormData>( | |
| } | ||
|
|
||
| if (!isSubmitted) { | ||
| setSubmitted(true); // User has attempted to submit the form at least once | ||
| setIsSubmitted(true); // User has attempted to submit the form at least once | ||
| } | ||
| setSubmitting(true); | ||
|
|
||
| const isFormValid = await validateFields(); | ||
| const isFormValid = await validateAllFields(); | ||
| const formData = serializer(getFormData() as T); | ||
|
|
||
| if (onSubmit) { | ||
| await onSubmit(formData, isFormValid); | ||
| await onSubmit(formData, isFormValid!); | ||
| } | ||
|
|
||
| setSubmitting(false); | ||
|
|
||
| return { data: formData, isValid: isFormValid }; | ||
| return { data: formData, isValid: isFormValid! }; | ||
| }; | ||
|
|
||
| const subscribe: FormHook<T>['subscribe'] = handler => { | ||
| const format = () => serializer(getFormData() as T); | ||
| const validate = async () => await validateAllFields(); | ||
|
|
||
| const subscription = formData$.current.subscribe(raw => { | ||
| handler({ isValid, data: { raw, format }, validate }); | ||
| }); | ||
|
|
||
| formUpdateSubscribers.current.push(subscription); | ||
| return subscription; | ||
| }; | ||
|
|
||
| /** | ||
| * 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'] = () => { | ||
| Object.entries(fieldsRefs.current).forEach(([path, field]) => { | ||
| field.reset(); | ||
| }); | ||
| setIsSubmitted(false); | ||
| setSubmitting(false); | ||
| setIsValid(undefined); | ||
| }; | ||
|
|
||
| const form: FormHook<T> = { | ||
| isSubmitted, | ||
| isSubmitting, | ||
| isValid, | ||
| submit: submitForm, | ||
| subscribe, | ||
| setFieldValue, | ||
| setFieldErrors, | ||
| getFields, | ||
| getFormData, | ||
| getFieldDefaultValue, | ||
| reset, | ||
| __options: formOptions, | ||
| __formData$: formData$, | ||
| __updateFormDataAt: updateFormDataAt, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| @import "./components/mappings_editor/styles" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo:
fiels