Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/five-peas-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'formik': minor
'fdocs3': patch
---

feat(formik): add native HTML5 validation support to Form and handleSubmit
19 changes: 18 additions & 1 deletion packages/formik/src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type FormikFormProps = Pick<
>
>;


// Type alias for a standard <form> element props without refs
type FormProps = React.ComponentPropsWithoutRef<'form'>;

// @todo tests
Expand All @@ -18,17 +20,32 @@ export const Form = React.forwardRef<HTMLFormElement, FormProps>(
// We default the action to "#" in case the preventDefault fails (just updates the URL hash)
const { action, ...rest } = props;
const _action = action ?? '#';

// Get Formik handlers from context
const { handleReset, handleSubmit } = useFormikContext();

return (
<form
onSubmit={handleSubmit}
ref={ref}
onReset={handleReset}
action={_action}
onSubmit={(event) => {
// Run native HTML5 validation first
const form = event.currentTarget;
if (!form.reportValidity()) {
// Stop Formik from submitting if native validation fails
event.preventDefault();
return;
}

// Proceed with Formik submit
handleSubmit(event);
}}
{...rest}
/>
);
}
);

// For better DevTools display name
Form.displayName = 'Form';
71 changes: 40 additions & 31 deletions packages/formik/src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -804,43 +804,52 @@ export function useFormik<Values extends FormikValues = FormikValues>({
);
});



// This change ensures:
// Proper native validation (e.g., HTML5 required fields)
// Defensive programming with event handling
// Developer guidance for proper button usage
// Controlled async submit error logging

const handleSubmit = useEventCallback(
(e?: React.FormEvent<HTMLFormElement>) => {
if (e && e.preventDefault && isFunction(e.preventDefault)) {
e.preventDefault();
}
(e?: React.FormEvent<HTMLFormElement>) => {
const form = e?.currentTarget;
if (form && typeof form.reportValidity === 'function' && !form.reportValidity()) {
return;
}

if (e && e.stopPropagation && isFunction(e.stopPropagation)) {
e.stopPropagation();
}

// Warn if form submission is triggered by a <button> without a
// specified `type` attribute during development. This mitigates
// a common gotcha in forms with both reset and submit buttons,
// where the dev forgets to add type="button" to the reset button.
if (__DEV__ && typeof document !== 'undefined') {
// Safely get the active element (works with IE)
const activeElement = getActiveElement();
if (
activeElement !== null &&
activeElement instanceof HTMLButtonElement
) {
invariant(
activeElement.attributes &&
activeElement.attributes.getNamedItem('type'),
'You submitted a Formik form using a button with an unspecified `type` attribute. Most browsers default button elements to `type="submit"`. If this is not a submit button, please add `type="button"`.'
);
}
}
if (e && e.preventDefault && isFunction(e.preventDefault)) {
e.preventDefault();
}

submitForm().catch(reason => {
console.warn(
`Warning: An unhandled error was caught from submitForm()`,
reason
if (e && e.stopPropagation && isFunction(e.stopPropagation)) {
e.stopPropagation();
}
if (__DEV__ && typeof document !== 'undefined') {
const activeElement = getActiveElement();
if (
activeElement !== null &&
activeElement instanceof HTMLButtonElement
) {
invariant(
activeElement.attributes &&
activeElement.attributes.getNamedItem('type'),
'You submitted a Formik form using a button with an unspecified `type` attribute. Most browsers default button elements to `type="submit"`. If this is not a submit button, please add `type="button"`.'
);
});
}
}
);

submitForm().catch(reason => {
console.warn(
`Warning: An unhandled error was caught from submitForm()`,
reason
);
});
}
);


const imperativeMethods: FormikHelpers<Values> = {
resetForm,
Expand Down
129 changes: 129 additions & 0 deletions website/src/pages/testsubmit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @component TestSubmit
* @description
* A styled React Form component using Formik and Yup for form state management and validation.
* This form includes two fields: Email and Age, both of which are required.
*
* Features:
* - Email validation using Yup's `.email()` and `.required()` rules.
* - Age validation that ensures:
* - the input is numeric (transforms empty string to NaN),
* - it's a whole number (integer),
* - it's positive,
* - and it's required.
* - Inline styles for a clean and modern appearance.
* - Error messages shown below each field using <ErrorMessage>.
* - Debug info (`isValid`, `touched`, `errors`) displayed using <pre> block.
* - Form resets on successful submission.
*
* @usage
* <TestSubmit />
*
* @note
* - Age input uses Yup's `.transform()` to handle empty strings correctly, ensuring validation works with edge cases like decimal input.
* - All logic is contained in a single component for simplicity.
*/



import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

// Validation Schema
const validationSchema = Yup.object({
email: Yup.string()
.email('Invalid email format')
.required('Email is required'),
age: Yup.number()
.transform((value, originalValue) =>
String(originalValue).trim() === '' ? NaN : Number(originalValue)
)
.typeError('Age must be a number')
.integer('Age must be a whole number')
.positive('Age must be a positive number')
.required('Age is required'),
});

const styles = {
formContainer: {
maxWidth: '400px',
margin: '40px auto',
padding: '24px',
border: '1px solid #ccc',
borderRadius: '10px',
fontFamily: 'Arial, sans-serif',
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
backgroundColor: '#f9f9f9',
},
fieldGroup: {
marginBottom: '16px',
},
label: {
display: 'block',
marginBottom: '6px',
fontWeight: 'bold',
},
input: {
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
},
errorText: {
color: 'red',
fontSize: '12px',
marginTop: '4px',
},
submitButton: {
padding: '10px 20px',
backgroundColor: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
},
};

const TestSubmit = () => {
return (
<Formik
initialValues={{ email: '', age: '' }}
validationSchema={validationSchema}
onSubmit={(values, { resetForm }) => {
console.log('✅ Form submitted successfully:', values);
alert('Form submitted successfully');
resetForm();
}}
>
{({ isValid, touched, errors }) => (
<Form style={styles.formContainer}>
<div style={styles.fieldGroup}>
<label style={styles.label}>Email:</label>
<Field name="email" type="email" style={styles.input} />
<ErrorMessage name="email">
{msg => <div style={styles.errorText}>{msg}</div>}
</ErrorMessage>
</div>

<div style={styles.fieldGroup}>
<label style={styles.label}>Age:</label>
<Field name="age" type="number" style={styles.input} />
<ErrorMessage name="age">
{msg => <div style={styles.errorText}>{msg}</div>}
</ErrorMessage>
</div>

<button type="submit" style={styles.submitButton}>
Submit
</button>

{/* Debug output */}
<pre>{JSON.stringify({ isValid, touched, errors }, null, 2)}</pre>
</Form>
)}
</Formik>
);
};

export default TestSubmit;