@@ -5,249 +5,21 @@ import {
55 appendExtraData ,
66 type Client ,
77 type FieldValues ,
8- type SubmissionError ,
9- type SubmissionSuccess ,
108} from '@formspree/core' ;
9+ import type { FormConfig , FormContext , FormHandle } from './types' ;
1110import {
12- DataAttributes ,
13- type FormConfig ,
14- type FormContext ,
15- type FormElement ,
16- type FormHandle ,
17- type MessageType ,
18- } from './types' ;
19-
20- const getFormElement = ( elementOrSelector : FormElement ) : HTMLFormElement => {
21- if ( typeof elementOrSelector === 'string' ) {
22- const element = document . querySelector ( elementOrSelector ) ;
23- if ( ! element ) {
24- throw new Error ( `Element "${ elementOrSelector } " not found` ) ;
25- }
26- if ( ! ( element instanceof HTMLFormElement ) ) {
27- throw new Error ( `Element "${ elementOrSelector } " is not a form element` ) ;
28- }
29- return element ;
30- }
31- return elementOrSelector ;
32- } ;
33-
34- /**
35- * Internal marker attribute. Set on elements whose text content was injected
36- * by the library (i.e. the element was originally empty). Used to know when
37- * it is safe to clear the text on hide — user-provided content is never cleared.
38- */
39- const SERVER_CONTENT_ATTR = 'data-fs-server-content' ;
40-
41- /**
42- * Shows an element by setting `data-fs-active`.
43- * If the element is empty and a message is provided, the message is injected
44- * and the element is marked with `data-fs-server-content` so it can be cleared later.
45- */
46- const showElement = ( element : HTMLElement , message ?: string ) : void => {
47- if ( message && element . textContent ?. trim ( ) === '' ) {
48- element . textContent = message ;
49- element . setAttribute ( SERVER_CONTENT_ATTR , '' ) ;
50- }
51- element . setAttribute ( DataAttributes . ACTIVE , '' ) ;
52- } ;
53-
54- /**
55- * Hides an element by removing `data-fs-active`.
56- * If the text was injected by the library, it is cleared.
57- */
58- const hideElement = ( element : HTMLElement ) : void => {
59- element . removeAttribute ( DataAttributes . ACTIVE ) ;
60- if ( element . hasAttribute ( SERVER_CONTENT_ATTR ) ) {
61- element . textContent = '' ;
62- element . removeAttribute ( SERVER_CONTENT_ATTR ) ;
63- }
64- } ;
65-
66- const defaultOnSuccess = < T extends FieldValues > (
67- context : FormContext < T > ,
68- _result : SubmissionSuccess
69- ) : void => {
70- const { form } = context ;
71- const replacement = document . createElement ( 'div' ) ;
72- replacement . textContent = 'Thank you!' ;
73- form . parentNode ?. replaceChild ( replacement , form ) ;
74- } ;
75-
76- /**
77- * Default implementation to enable submit buttons.
78- * Restores original button text and re-enables the button.
79- */
80- const defaultEnable = < T extends FieldValues > (
81- context : FormContext < T >
82- ) : void => {
83- const buttons = context . form . querySelectorAll < HTMLButtonElement > (
84- `[type='submit']:disabled, [${ DataAttributes . SUBMIT_BTN } ]:disabled`
85- ) ;
86- buttons . forEach ( ( button ) => {
87- const originalText = button . dataset . fsOriginalText ;
88- if ( originalText ) {
89- button . textContent = originalText ;
90- delete button . dataset . fsOriginalText ;
91- }
92- button . disabled = false ;
93- } ) ;
94- } ;
95-
96- /**
97- * Default implementation to disable submit buttons.
98- * Saves original button text, shows a loading spinner, and disables the button.
99- */
100- const defaultDisable = < T extends FieldValues > (
101- context : FormContext < T >
102- ) : void => {
103- const buttons = context . form . querySelectorAll < HTMLButtonElement > (
104- `[type='submit']:enabled, [${ DataAttributes . SUBMIT_BTN } ]:enabled`
105- ) ;
106- buttons . forEach ( ( button ) => {
107- button . dataset . fsOriginalText = button . textContent ?? '' ;
108- button . disabled = true ;
109- } ) ;
110- } ;
111-
112- /**
113- * Default implementation to render field-level validation errors in the DOM.
114- *
115- * - Finds elements with `data-fs-error="fieldName"` and shows/hides them.
116- * If the element is empty, injects the first error message from the API.
117- * If the element has user-provided content, shows it as-is.
118- * - Sets `aria-invalid="true"` on `data-fs-field` inputs whose `name` matches an errored field.
119- */
120- const defaultRenderFieldErrors = < T extends FieldValues > (
121- context : FormContext < T > ,
122- error : SubmissionError < T > | null
123- ) : void => {
124- const { form } = context ;
125-
126- // Handle field error elements (data-fs-error="fieldName")
127- const errorElements = form . querySelectorAll < HTMLElement > (
128- `[${ DataAttributes . ERROR } ]`
129- ) ;
130-
131- errorElements . forEach ( ( element ) => {
132- const fieldName = element . dataset . fsError ;
133-
134- // Skip form-level error elements (no value) — handled by renderFormMessage
135- if ( ! fieldName ) return ;
136-
137- if ( ! error ) {
138- hideElement ( element ) ;
139- return ;
140- }
141-
142- const fieldErrors = error . getFieldErrors ( fieldName as keyof T ) ;
143-
144- if ( fieldErrors . length === 0 ) {
145- hideElement ( element ) ;
146- return ;
147- }
148-
149- showElement ( element , fieldErrors [ 0 ] . message ) ;
150- } ) ;
151-
152- // Handle field elements (aria-invalid)
153- const fieldElements = form . querySelectorAll < HTMLElement > (
154- `[${ DataAttributes . FIELD } ]`
155- ) ;
156-
157- fieldElements . forEach ( ( element ) => {
158- const fieldName = element . getAttribute ( 'name' ) ;
159-
160- if ( ! fieldName ) {
161- element . removeAttribute ( 'aria-invalid' ) ;
162- return ;
163- }
164-
165- if ( ! error ) {
166- element . removeAttribute ( 'aria-invalid' ) ;
167- return ;
168- }
169-
170- const fieldErrors = error . getFieldErrors ( fieldName as keyof T ) ;
171-
172- if ( fieldErrors . length === 0 ) {
173- element . removeAttribute ( 'aria-invalid' ) ;
174- return ;
175- }
176-
177- element . setAttribute ( 'aria-invalid' , 'true' ) ;
178- } ) ;
179- } ;
180-
181- /**
182- * Finds an element by attribute, searching inside the form first,
183- * then in the form's parent container.
184- */
185- const findElement = (
186- form : HTMLFormElement ,
187- selector : string
188- ) : HTMLElement | null => {
189- return (
190- form . querySelector < HTMLElement > ( selector ) ??
191- form . parentElement ?. querySelector < HTMLElement > ( selector ) ??
192- null
193- ) ;
194- } ;
195-
196- /**
197- * Finds the success element (`data-fs-success`) for a form.
198- */
199- const findSuccessElement = ( form : HTMLFormElement ) : HTMLElement | null => {
200- return findElement ( form , `[${ DataAttributes . SUCCESS } ]` ) ;
201- } ;
202-
203- /**
204- * Finds the form-level error element (`data-fs-error` without a value) for a form.
205- */
206- const findFormErrorElement = ( form : HTMLFormElement ) : HTMLElement | null => {
207- return findElement ( form , `[${ DataAttributes . ERROR } =""]` ) ;
208- } ;
209-
210- /**
211- * Default implementation to render form-level messages in the DOM.
212- *
213- * - On success: shows the `data-fs-success` element.
214- * - On error: shows the `data-fs-error` element (without a value).
215- * - If the element is empty, injects the provided message.
216- * - If the element has user-provided content, shows it as-is.
217- */
218- const defaultRenderFormMessage = < T extends FieldValues > (
219- context : FormContext < T > ,
220- type : MessageType | null ,
221- message : string | null
222- ) : void => {
223- const successEl = findSuccessElement ( context . form ) ;
224- const formErrorEl = findFormErrorElement ( context . form ) ;
225-
226- if ( type === null ) {
227- if ( successEl ) hideElement ( successEl ) ;
228- if ( formErrorEl ) hideElement ( formErrorEl ) ;
229- return ;
230- }
231-
232- if ( type === 'success' ) {
233- if ( formErrorEl ) hideElement ( formErrorEl ) ;
234- if ( successEl ) showElement ( successEl , message ?? undefined ) ;
235- } else {
236- if ( successEl ) hideElement ( successEl ) ;
237- if ( formErrorEl ) showElement ( formErrorEl , message ?? undefined ) ;
238- }
239- } ;
240-
241- /**
242- * Builds a human-readable error message from a SubmissionError.
243- * Only includes form-level errors; field errors are displayed inline via `data-fs-error` elements.
244- */
245- const buildErrorMessage = < T extends FieldValues > (
246- error : SubmissionError < T >
247- ) : string => {
248- const formErrors = error . getFormErrors ( ) . map ( ( e ) => e . message ) ;
249- return formErrors . join ( ', ' ) || 'There was an error submitting the form.' ;
250- } ;
11+ buildErrorMessage ,
12+ DEFAULT_ENDPOINT ,
13+ defaultDisable ,
14+ defaultEnable ,
15+ defaultOnSuccess ,
16+ defaultRenderFieldErrors ,
17+ defaultRenderFormMessage ,
18+ findSuccessElement ,
19+ getFormElement ,
20+ injectDefaultStyles ,
21+ log ,
22+ } from './utils' ;
25123
25224const handleSubmit = async < T extends FieldValues > (
25325 context : FormContext < T >
@@ -337,73 +109,6 @@ const handleSubmit = async <T extends FieldValues>(
337109 }
338110} ;
339111
340- const log = ( message : string , data ?: unknown ) : void => {
341- console . log ( `[formspree-ajax] ${ message } ` , data ?? '' ) ;
342- } ;
343-
344- const DEFAULT_ENDPOINT = 'https://formspree.io' ;
345-
346- /**
347- * Flag to track whether default styles have been injected.
348- */
349- let stylesInjected = false ;
350-
351- /**
352- * Injects default Formspree styles into the document head.
353- * Only runs once per page load.
354- */
355- const injectDefaultStyles = ( ) : void => {
356- if ( stylesInjected || typeof document === 'undefined' ) {
357- return ;
358- }
359-
360- const styleElement = document . createElement ( 'style' ) ;
361- styleElement . setAttribute ( 'data-formspree-styles' , '' ) ;
362- styleElement . textContent = `
363- [${ DataAttributes . ERROR } ],
364- [${ DataAttributes . SUCCESS } ] {
365- display: none;
366- }
367-
368- [${ DataAttributes . ERROR } ][${ DataAttributes . ACTIVE } ] {
369- display: block;
370- color: #dc3545;
371- }
372-
373- [${ DataAttributes . ERROR } =""][${ DataAttributes . ACTIVE } ] {
374- padding: 12px;
375- border-radius: 8px;
376- margin-bottom: 20px;
377- font-size: 14px;
378- background: #f8d7da;
379- border: 1px solid #f5c6cb;
380- }
381-
382- [${ DataAttributes . ERROR } ]:not([${ DataAttributes . ERROR } =""])[${ DataAttributes . ACTIVE } ] {
383- font-size: 12px;
384- margin-top: 4px;
385- }
386-
387- [${ DataAttributes . SUCCESS } ][${ DataAttributes . ACTIVE } ] {
388- display: block;
389- padding: 12px;
390- border-radius: 8px;
391- margin-bottom: 20px;
392- font-size: 14px;
393- background: #d4edda;
394- color: #155724;
395- border: 1px solid #c3e6cb;
396- }
397-
398- [${ DataAttributes . FIELD } ][aria-invalid="true"] {
399- border-color: #dc3545;
400- }
401- ` ;
402-
403- document . head . appendChild ( styleElement ) ;
404- stylesInjected = true ;
405- } ;
406-
407112export const initForm = < T extends FieldValues = FieldValues > (
408113 config : FormConfig < T >
409114) : FormHandle => {
0 commit comments