
FetchIt
Lightweight AJAX form submission in MODX via the Fetch API on top of FormIt or a custom snippet


Valibot describes the form rules with a schema, like yup or Zod, but is built from separate functions. For a form with three fields the browser gets 2–3 KB.
Important!
Client-side validation is not secure and should only be used to improve user experience.
novalidate turns off the browser popups: FetchIt shows the errors in [data-error].
<form action="[[~[[*id]]]]" method="post" novalidate>
<label> Name
<input type="text" name="name" value="[[+fi.name]]">
<span data-error="name">[[+fi.error.name]]</span>
</label>
<label> E-mail
<input type="email" name="email" value="[[+fi.email]]">
<span data-error="email">[[+fi.error.email]]</span>
</label>
<label>
<input type="checkbox" name="agree" value="1">
I agree to the processing of my personal data
</label>
<span data-error="agree">[[+fi.error.agree]]</span>
<div role="alert" data-success style="display: none;"></div>
<div role="alert" data-validation-error style="display: none;"></div>
<button type="submit">Send</button>
</form><form action="{$_modx->resource.id | url}" method="post" novalidate>
<label> Name
<input type="text" name="name" value="{$_modx->getPlaceholder('fi.name')}">
<span data-error="name">{$_modx->getPlaceholder('fi.error.name')}</span>
</label>
<label> E-mail
<input type="email" name="email" value="{$_modx->getPlaceholder('fi.email')}">
<span data-error="email">{$_modx->getPlaceholder('fi.error.email')}</span>
</label>
<label>
<input type="checkbox" name="agree" value="1">
I agree to the processing of my personal data
</label>
<span data-error="agree">{$_modx->getPlaceholder('fi.error.agree')}</span>
<div role="alert" data-success style="display: none;"></div>
<div role="alert" data-validation-error style="display: none;"></div>
<button type="submit">Send</button>
</form><script type="module">
import * as v from 'https://cdn.jsdelivr.net/npm/valibot@1/+esm'
const ContactSchema = v.object({
name: v.pipe(
v.string(),
v.trim(),
v.nonEmpty('What should we call you?'),
v.minLength(2, 'The name needs at least two letters'),
),
email: v.pipe(
v.string(),
v.trim(),
v.nonEmpty('Enter your e-mail'),
v.email('The address seems to have a typo'),
),
agree: v.pipe(
v.optional(v.string(), ''),
v.value('1', 'We cannot reply without your consent'),
),
})
document.addEventListener('fetchit:before', (e) => {
const { formData, fetchit } = e.detail
const result = v.safeParse(ContactSchema, Object.fromEntries(formData))
if (result.success) {
return
}
e.preventDefault()
const errors = v.flatten(result.issues).nested ?? {}
for (const [name, [message]] of Object.entries(errors)) {
fetchit.setError(name, message)
}
fetchit.getFields(Object.keys(errors)[0])[0]?.focus()
})
</script>Object.fromEntries(formData) collects the fields into an object. v.object lets extra fields through — the service fields of FetchIt and of the spam protection.formData at all. v.optional(v.string(), '') puts an empty string in its place, and v.value shows its own message. Without optional Valibot would answer with a technical text about a missing key.v.flatten sorts the errors by field name. The first message is taken for each field.defer, so DOMContentLoaded is not needed: the handler is in place before the visitor has time to send the form.Client-side checks only help the visitor. The same rules are needed on the server — in FormIt or in your own snippet:
[[!FetchIt?
&form=`myForm.tpl`
&hooks=`email`
&validate=`name:required:minLength=^2^,email:required:email,agree:required`
]]{'!FetchIt' | snippet : [
'form' => 'myForm.tpl',
'hooks' => 'email',
'validate' => 'name:required:minLength=^2^,email:required:email,agree:required',
]}FormIt validators: FormIt documentation.