Every AJAX trigger you've built so far has responded to a select element's change event. But #ajax isn't limited to selects — it works on submit buttons too, and you've actually already seen it: the "Add one more" and "Remove one" buttons from the earlier add-more lesson were both submit buttons with an #ajax property attached. This lesson looks at that combination directly, and at a genuinely different use case: an AJAX-only submit that never triggers a real form submission at all.
What you'll learn in this lesson
- Why a button can carry both
#submitand#ajaxat once, and what order they run in - The difference between a
submitbutton and a plainbuttonelement for AJAX purposes - How to build a button that only ever does AJAX, and never a real page submission
- Why the button's own
#typechoice matters more than it looks like it should
Revisiting the button you've already used
$form['names_fieldset']['actions']['add_name'] = [
'#type' => 'submit',
'#value' => $this->t('Add one more'),
'#submit' => ['::addOne'],
'#ajax' => [
'callback' => '::addMoreCallback',
'wrapper' => 'names-fieldset-wrapper',
],
];
This is real code from the earlier add-more lesson, and it already answers the "how" question: a #type => 'submit' button can carry #submit and #ajax simultaneously, with the #submit handler running first and the #ajax callback running second, once the form has been rebuilt with whatever the submit handler changed.
#type: 'button' — a submit that never really submits
$form['preview'] = [
'#type' => 'button',
'#value' => $this->t('Preview'),
'#ajax' => [
'callback' => '::previewCallback',
'wrapper' => 'preview-wrapper',
],
];
This is the genuinely different case: #type => 'button' instead of #type => 'submit'. A plain button element never triggers Drupal's normal form-submission pipeline at all — no validateForm(), no submitForm(), ever, no matter what. Combined with #ajax, it becomes a control whose sole purpose is triggering an AJAX callback: a "Preview" button that renders a preview without ever actually submitting anything, or a "Check availability" button that just queries something and reports back.
#type => 'submit' + #ajax gives you both a real submission path (with #submit handlers) and an AJAX response, running in that order. #type => 'button' + #ajax gives you AJAX only — there is no submission path at all, by design, regardless of what you set #submit to. Choosing the wrong one is a common source of "why did my form submit when I only wanted a preview" bugs.Quick check: you want a "Check username availability" button that queries the database and shows a result, but should never actually submit the surrounding registration form. Which
#typedo you reach for? (Answer:#type => 'button', not'submit'— a plain button with#ajaxnever enters the form-submission pipeline at all, which is exactly the guarantee you need here.)
Key takeaways
- A
#type => 'submit'button can carry both#submitand#ajax— the submit handler runs first, then the AJAX callback runs against the rebuilt form. #type => 'button'never enters the normal form-submission pipeline, with or without#ajaxattached — it's purely a trigger for the AJAX callback.- Reach for
'button'whenever you want an action that should never, under any circumstance, submit the surrounding form. - Reach for
'submit'+#ajaxwhenever you genuinely want both a real submission and an AJAX-driven update, like the add-more pattern.
Coming up next
You've now covered AJAX from every angle — callbacks, commands, dynamic fields, error handling, and submit buttons. The next lesson moves to a related but distinct tool for reactive forms: the #states property, which lets you show, hide, and enable fields conditionally with zero PHP round-trips at all.