begins form typing

This commit is contained in:
James
2020-11-30 10:59:57 -05:00
parent 20fabb81e8
commit f873a84f08
21 changed files with 234 additions and 130 deletions

View File

@@ -1,4 +1,7 @@
const buildValidationPromise = async (fieldState, field) => {
import { Field as FieldSchema } from '../../../../fields/config/types';
import { Fields, Field, Data } from './types';
const buildValidationPromise = async (fieldState: Field, field: FieldSchema) => {
const validatedFieldState = fieldState;
validatedFieldState.valid = typeof field.validate === 'function' ? await field.validate(fieldState.value, field) : true;
@@ -9,7 +12,7 @@ const buildValidationPromise = async (fieldState, field) => {
}
};
const buildStateFromSchema = async (fieldSchema, fullData = {}) => {
const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = {}): Promise<Fields> => {
if (fieldSchema) {
const validationPromises = [];
@@ -19,6 +22,7 @@ const buildStateFromSchema = async (fieldSchema, fullData = {}) => {
const fieldState = {
value,
initialValue: value,
valid: true,
};
validationPromises.push(buildValidationPromise(fieldState, field));
@@ -26,7 +30,7 @@ const buildStateFromSchema = async (fieldSchema, fullData = {}) => {
return fieldState;
};
const iterateFields = (fields, data, path = '') => fields.reduce((state, field) => {
const iterateFields = (fields: FieldSchema[], data: Data, path = '') => fields.reduce((state, field) => {
let initialData = data;
if (field.name && field.defaultValue && typeof initialData?.[field.name] === 'undefined') {

View File

@@ -0,0 +1,27 @@
import { createContext, useContext } from 'react';
import { Context as FormContext } from './types';
const FormContext = createContext({} as FormContext);
const FormWatchContext = createContext({} as FormContext);
const SubmittedContext = createContext(false);
const ProcessingContext = createContext(false);
const ModifiedContext = createContext(false);
const useForm = (): FormContext => useContext(FormContext);
const useWatchForm = (): FormContext => useContext(FormWatchContext);
const useFormSubmitted = (): boolean => useContext(SubmittedContext);
const useFormProcessing = (): boolean => useContext(ProcessingContext);
const useFormModified = (): boolean => useContext(ModifiedContext);
export {
FormContext,
FormWatchContext,
SubmittedContext,
ProcessingContext,
ModifiedContext,
useForm,
useWatchForm,
useFormSubmitted,
useFormProcessing,
useFormModified,
};

View File

@@ -1,26 +0,0 @@
import { createContext, useContext } from 'react';
const FormContext = createContext({});
const FieldContext = createContext({});
const SubmittedContext = createContext(false);
const ProcessingContext = createContext(false);
const ModifiedContext = createContext(false);
const useForm = () => useContext(FormContext);
const useFormFields = () => useContext(FieldContext);
const useFormSubmitted = () => useContext(SubmittedContext);
const useFormProcessing = () => useContext(ProcessingContext);
const useFormModified = () => useContext(ModifiedContext);
export {
FormContext,
FieldContext,
SubmittedContext,
ProcessingContext,
ModifiedContext,
useForm,
useFormFields,
useFormSubmitted,
useFormProcessing,
useFormModified,
};

View File

@@ -1,7 +1,8 @@
import { unflatten, flatten } from 'flatley';
import flattenFilters from './flattenFilters';
import { Fields } from './types';
const unflattenRowsFromState = (state, path) => {
const unflattenRowsFromState = (state: Fields, path) => {
// Take a copy of state
const remainingFlattenedState = { ...state };
@@ -32,7 +33,7 @@ const unflattenRowsFromState = (state, path) => {
};
};
function fieldReducer(state, action) {
function fieldReducer(state: Fields, action): Fields {
switch (action.type) {
case 'REPLACE_STATE': {
return action.state;

View File

@@ -1,5 +1,5 @@
const flattenFilters = [{
test: (_, value) => {
test: (_: string, value: unknown): boolean => {
const hasValidProperty = Object.prototype.hasOwnProperty.call(value, 'valid');
const hasValueProperty = Object.prototype.hasOwnProperty.call(value, 'value');

View File

@@ -1,7 +1,8 @@
import { unflatten } from 'flatley';
import reduceFieldsToValues from './reduceFieldsToValues';
import { Fields } from './types';
const getDataByPath = (fields, path) => {
const getDataByPath = (fields: Fields, path: string): unknown => {
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
const name = path.split('.').pop();

View File

@@ -1,6 +1,7 @@
import reduceFieldsToValues from './reduceFieldsToValues';
import { Fields, Data } from './types';
const getSiblingData = (fields, path) => {
const getSiblingData = (fields: Fields, path: string): Data => {
let siblingFields = fields;
// If this field is nested

View File

@@ -3,7 +3,6 @@ import React, {
} from 'react';
import { objectToFormData } from 'object-to-formdata';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { toast } from 'react-toastify';
import { useAuth } from '@payloadcms/config-provider';
import { useLocale } from '../../utilities/Locale';
@@ -17,14 +16,15 @@ import getDataByPathFunc from './getDataByPath';
import wait from '../../../../utilities/wait';
import buildInitialState from './buildInitialState';
import errorMessages from './errorMessages';
import { Context as FormContextType, Props } from './types';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormWatchContext } from './context';
import './index.scss';
const baseClass = 'form';
const Form: React.FC = (props) => {
const Form: React.FC<Props> = (props) => {
const {
disabled,
onSubmit,
@@ -51,7 +51,7 @@ const Form: React.FC = (props) => {
const [submitted, setSubmitted] = useState(false);
const [formattedInitialData, setFormattedInitialData] = useState(buildInitialState(initialData));
const contextRef = useRef({ ...initContextState });
const contextRef = useRef({} as FormContextType);
let initialFieldState = {};
@@ -72,10 +72,11 @@ const Form: React.FC = (props) => {
const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => {
const validatedField = { ...field };
validatedField.valid = typeof field.validate === 'function' ? await field.validate(field.value) : true;
validatedField.valid = true;
const validationResult = typeof field.validate === 'function' ? await field.validate(field.value) : true;
if (typeof validatedField.valid === 'string') {
validatedField.errorMessage = validatedField.valid;
if (typeof validationResult === 'string') {
validatedField.errorMessage = validationResult;
validatedField.valid = false;
isValid = false;
}
@@ -90,10 +91,10 @@ const Form: React.FC = (props) => {
return isValid;
}, [contextRef]);
const submit = useCallback(async (e) => {
const submit = useCallback(async (e): Promise<void> => {
if (disabled) {
e.preventDefault();
return false;
return;
}
e.stopPropagation();
@@ -111,12 +112,13 @@ const Form: React.FC = (props) => {
if (!isValid) {
toast.error('Please correct invalid fields.');
return false;
return;
}
// If submit handler comes through via props, run that
if (onSubmit) {
return onSubmit(fields, reduceFieldsToValues(fields));
onSubmit(fields, reduceFieldsToValues(fields));
return;
}
const formData = contextRef.current.createFormData();
@@ -128,15 +130,18 @@ const Form: React.FC = (props) => {
setModified(false);
if (typeof handleResponse === 'function') return handleResponse(res);
if (typeof handleResponse === 'function') {
handleResponse(res);
return;
}
setProcessing(false);
const contentType = res.headers.get('content-type');
const isJSON = contentType && contentType.indexOf('application/json') !== -1;
let json = {};
let json: any = {};
if (isJSON) json = await res.json();
if (res.status < 400) {
@@ -147,9 +152,10 @@ const Form: React.FC = (props) => {
if (redirect) {
const destination = {
pathname: redirect,
state: {},
};
if (json.message && !disableSuccessStatus) {
if (typeof json === 'object' && json.message && !disableSuccessStatus) {
destination.state = {
status: [
{
@@ -170,16 +176,42 @@ const Form: React.FC = (props) => {
if (json.message) {
toast.error(json.message);
return json;
return;
}
if (Array.isArray(json.errors)) {
const [fieldErrors, nonFieldErrors] = (errors) => errors.reduce(([fieldErrs, nonFieldErrs], err) => {
if (err.data) {
return [[...fieldErrs, ...err.data], [...nonFieldErrs, err]];
}
return [fieldErrs, [...nonFieldErrs, err]];
}, [[], []]);
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
([fieldErrs, nonFieldErrs], err) => {
const newFieldErrs = [];
const newNonFieldErrs = [];
if (err?.message) {
newNonFieldErrs.push(err);
}
if (Array.isArray(err?.data)) {
err.data.forEach((dataError) => {
if (dataError?.field) {
newFieldErrs.push(dataError);
} else {
newNonFieldErrs.push(dataError);
}
});
}
return [
[
...fieldErrs,
...newFieldErrs,
],
[
...nonFieldErrs,
...newNonFieldErrs,
],
];
},
[[], []],
);
fieldErrors.forEach((err) => {
dispatchFields({
@@ -194,7 +226,7 @@ const Form: React.FC = (props) => {
toast.error(err.message || 'An unknown error occurred.');
});
return json;
return;
}
const message = errorMessages[res.status] || 'An unknown error occurrred.';
@@ -202,7 +234,7 @@ const Form: React.FC = (props) => {
toast.error(message);
}
return json;
return;
} catch (err) {
setProcessing(false);
@@ -224,10 +256,10 @@ const Form: React.FC = (props) => {
const getFields = useCallback(() => contextRef.current.fields, [contextRef]);
const getField = useCallback((path) => contextRef.current.fields[path], [contextRef]);
const getField = useCallback((path: string) => contextRef.current.fields[path], [contextRef]);
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]);
const getSiblingData = useCallback((path) => getSiblingDataFunc(contextRef.current.fields, path), [contextRef]);
const getDataByPath = useCallback((path) => getDataByPathFunc(contextRef.current.fields, path), [contextRef]);
const getSiblingData = useCallback((path: string) => getSiblingDataFunc(contextRef.current.fields, path), [contextRef]);
const getDataByPath = useCallback((path: string) => getDataByPathFunc(contextRef.current.fields, path), [contextRef]);
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
const createFormData = useCallback(() => {
@@ -255,14 +287,14 @@ const Form: React.FC = (props) => {
useEffect(() => {
if (initialState) {
contextRef.current = { ...initContextState };
contextRef.current = { ...initContextState } as FormContextType;
dispatchFields({ type: 'REPLACE_STATE', state: initialState });
}
}, [initialState]);
useEffect(() => {
if (initialData) {
contextRef.current = { ...initContextState };
contextRef.current = { ...initContextState } as FormContextType;
const builtState = buildInitialState(initialData);
setFormattedInitialData(builtState);
dispatchFields({ type: 'REPLACE_STATE', state: builtState });
@@ -297,7 +329,7 @@ const Form: React.FC = (props) => {
className={classes}
>
<FormContext.Provider value={contextRef.current}>
<FieldContext.Provider value={{
<FormWatchContext.Provider value={{
fields,
...contextRef.current,
}}
@@ -309,46 +341,10 @@ const Form: React.FC = (props) => {
</ModifiedContext.Provider>
</ProcessingContext.Provider>
</SubmittedContext.Provider>
</FieldContext.Provider>
</FormWatchContext.Provider>
</FormContext.Provider>
</form>
);
};
Form.defaultProps = {
redirect: '',
onSubmit: null,
method: 'POST',
action: '',
handleResponse: null,
onSuccess: null,
className: '',
disableSuccessStatus: false,
disabled: false,
initialState: undefined,
waitForAutocomplete: false,
initialData: undefined,
log: false,
};
Form.propTypes = {
disableSuccessStatus: PropTypes.bool,
onSubmit: PropTypes.func,
method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']),
action: PropTypes.string,
handleResponse: PropTypes.func,
onSuccess: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
className: PropTypes.string,
redirect: PropTypes.string,
disabled: PropTypes.bool,
initialState: PropTypes.shape({}),
waitForAutocomplete: PropTypes.bool,
initialData: PropTypes.shape({}),
log: PropTypes.bool,
};
export default Form;

View File

@@ -0,0 +1,47 @@
import {
Fields,
Field,
Data,
DispatchFields,
Submit,
Context,
GetSiblingData,
GetUnflattenedValues,
ValidateForm,
CreateFormData,
SetModified,
SetProcessing,
SetSubmitted,
} from './types';
const submit: Submit = () => undefined;
const getSiblingData: GetSiblingData = () => undefined;
const getUnflattenedValues: GetUnflattenedValues = () => ({});
const dispatchFields: DispatchFields = () => undefined;
const validateForm: ValidateForm = () => undefined;
const createFormData: CreateFormData = () => undefined;
const setModified: SetModified = () => undefined;
const setProcessing: SetProcessing = () => undefined;
const setSubmitted: SetSubmitted = () => undefined;
const initialContextState: Context = {
getFields: (): Fields => ({ }),
getField: (): Field => undefined,
getData: (): Data => undefined,
getSiblingData,
getDataByPath: () => undefined,
getUnflattenedValues,
validateForm,
createFormData,
submit,
dispatchFields,
setModified,
setProcessing,
setSubmitted,
initialState: {},
fields: {},
disabled: false,
};
export default initialContextState;

View File

@@ -1,15 +0,0 @@
export default {
getFields: () => { },
getField: () => { },
getData: () => { },
getSiblingData: () => { },
getDataByPath: () => undefined,
getUnflattenedValues: () => { },
validateForm: () => { },
createFormData: () => { },
submit: () => { },
dispatchFields: () => { },
setModified: () => { },
initialState: {},
reset: 0,
};

View File

@@ -1,6 +1,7 @@
import { unflatten } from 'flatley';
import { Fields, Data } from './types';
const reduceFieldsToValues = (fields, flatten) => {
const reduceFieldsToValues = (fields: Fields, flatten?: boolean): Data => {
const data = {};
Object.keys(fields).forEach((key) => {

View File

@@ -0,0 +1,67 @@
export type Field = {
value: unknown
initialValue: unknown
errorMessage?: string
valid: boolean
validate?: (val: unknown) => Promise<boolean | string> | boolean | string
disableFormData?: boolean
ignoreWhileFlattening?: boolean
stringify?: boolean
}
export type Fields = {
[path: string]: Field
}
export type Data = {
[key: string]: unknown
}
export type Props = {
disabled?: boolean
onSubmit?: (fields: Fields, data: Data) => void
method?: 'GET' | 'PUT' | 'DELETE' | 'POST'
action?: string
handleResponse?: (res: Response) => void
onSuccess?: (json: unknown) => void
className?: string
redirect?: string
disableSuccessStatus?: boolean
initialState?: Fields
initialData?: Data
waitForAutocomplete?: boolean
log?: boolean
}
export type DispatchFields = React.Dispatch<any>
export type Submit = (e: React.FormEvent<HTMLFormElement>) => void;
export type ValidateForm = () => Promise<boolean>;
export type CreateFormData = () => FormData;
export type GetFields = () => Fields;
export type GetField = (path: string) => Field;
export type GetData = () => Data;
export type GetSiblingData = (path: string) => Data;
export type GetUnflattenedValues = () => Data;
export type GetDataByPath = (path: string) => unknown;
export type SetModified = (modified: boolean) => void;
export type SetSubmitted = (submitted: boolean) => void;
export type SetProcessing = (processing: boolean) => void;
export type Context = {
dispatchFields: DispatchFields
submit: Submit
fields: Fields
initialState: Fields
validateForm: ValidateForm
createFormData: CreateFormData
disabled: boolean
getFields: GetFields
getField: GetField
getData: GetData
getSiblingData: GetSiblingData
getUnflattenedValues: GetUnflattenedValues
getDataByPath: GetDataByPath
setModified: SetModified
setProcessing: SetProcessing
setSubmitted: SetSubmitted
}

View File

@@ -2,12 +2,12 @@ import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import { useFormFields } from '../../Form/context';
import { useWatchForm } from '../../Form/context';
import './index.scss';
const ConfirmPassword = () => {
const { getField } = useFormFields();
const { getField } = useWatchForm();
const password = getField('password');
const validate = useCallback((value) => {

View File

@@ -1,6 +1,6 @@
import React, { Fragment, useState, useEffect } from 'react';
import { useConfig, useAuth } from '@payloadcms/config-provider';
import { useFormFields } from '../../../../../../Form/context';
import { useWatchForm } from '../../../../../../Form/context';
import Relationship from '../../../../../Relationship';
import Number from '../../../../../Number';
import Select from '../../../../../Select';
@@ -26,7 +26,7 @@ const RelationshipFields = () => {
const [options, setOptions] = useState(() => createOptions(collections, permissions));
const { getData } = useFormFields();
const { getData } = useWatchForm();
const { relationTo } = getData();
useEffect(() => {

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useFormFields } from '../Form/context';
import { useWatchForm } from '../Form/context';
const withCondition = (Field) => {
const CheckForCondition = (props) => {
@@ -43,7 +43,7 @@ const withCondition = (Field) => {
const path = pathFromProps || name;
const { getData, getSiblingData, getField, dispatchFields } = useFormFields();
const { getData, getSiblingData, getField, dispatchFields } = useWatchForm();
const data = getData();
const field = getField(path);

View File

@@ -4,7 +4,7 @@ import useFieldType from '../../../../forms/useFieldType';
import Label from '../../../../forms/Label';
import CopyToClipboard from '../../../../elements/CopyToClipboard';
import { text } from '../../../../../../fields/validations';
import { useFormFields } from '../../../../forms/Form/context';
import { useWatchForm } from '../../../../forms/Form/context';
import './index.scss';
import GenerateConfirmation from '../../../../elements/GenerateConfirmation';
@@ -17,7 +17,7 @@ const APIKey = () => {
const [initialAPIKey, setInitialAPIKey] = useState(null);
const [highlightedField, setHighlightedField] = useState(false);
const { getField } = useFormFields();
const { getField } = useWatchForm();
const apiKey = getField(path);

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { toast } from 'react-toastify';
import { useConfig } from '@payloadcms/config-provider';
import Email from '../../../../forms/field-types/Email';
import Password from '../../../../forms/field-types/Password';
import Checkbox from '../../../../forms/field-types/Checkbox';
import Button from '../../../../elements/Button';
import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword';
import { useFormFields, useFormModified } from '../../../../forms/Form/context';
import { useConfig } from '@payloadcms/config-provider';
import { useWatchForm, useFormModified } from '../../../../forms/Form/context';
import APIKey from './APIKey';
@@ -18,7 +18,7 @@ const baseClass = 'auth-fields';
const Auth = (props) => {
const { useAPIKey, requirePassword, verify, collection: { slug }, email } = props;
const [changingPassword, setChangingPassword] = useState(requirePassword);
const { getField } = useFormFields();
const { getField } = useWatchForm();
const modified = useFormModified();
const enableAPIKey = getField('enableAPIKey');

View File

@@ -1,7 +1,7 @@
import { useFormFields } from '../components/forms/Form/context';
import { useWatchForm } from '../components/forms/Form/context';
const useTitle = (useAsTitle: string): string => {
const { getField } = useFormFields();
const { getField } = useWatchForm();
const titleField = getField(useAsTitle);
return titleField?.value;
};