Complete Production Example: ConfigSingleExportForm
When to Use
You want to see a complete, production-ready HTMX implementation demonstrating cascading selects, OOB updates, and history management.
Reference: /core/modules/config/src/Form/ConfigSingleExportForm.php — Drupal core's configuration export form
Form Structure
Two cascading selects (type → name) and an export textarea that updates based on selections.
Implementation Patterns
1. First Select Updates Second Select (lines 92-107)
// Config type select updates config name select
(new Htmx())
->post($form_url)
->onlyMainContent()
->select('*:has(>select[name="config_name"])') // Extract wrapper from response
->target('*:has(>select[name="config_name"])') // Replace wrapper in page
->swap('outerHTML')
->applyTo($form['config_type']);
2. Second Select Updates Display Region (lines 117-125)
// Config name select updates export textarea
(new Htmx())
->post($form_url)
->onlyMainContent()
->select('[data-export-wrapper]')
->target('[data-export-wrapper]')
->swap('outerHTML')
->applyTo($form['config_name']);
3. Out-of-Band Update (lines 141-143)
When type changes, clear the export textarea via OOB swap:
(new Htmx())
->swapOob('outerHTML:[data-export-wrapper]')
->applyTo($form['export'], '#wrapper_attributes');
4. Trigger Detection (line 137)
$trigger = $this->getHtmxTriggerName();
if ($trigger == 'config_type') {
$form = $this->updateConfigurationType($form, $form_state);
// Clear export
(new Htmx())
->swapOob('outerHTML:[data-export-wrapper]')
->applyTo($form['export'], '#wrapper_attributes');
$pushUrl = Url::fromRoute('config.export_single', [
'config_type' => $default_type,
'config_name' => '',
]);
}
elseif ($trigger == 'config_name') {
$default_name = $form_state->getValue('config_name', $config_name);
$form['export'] = $this->updateExport($form, $default_type, $default_name);
$pushUrl = Url::fromRoute('config.export_single', [
'config_type' => $default_type,
'config_name' => $default_name,
]);
}
5. Browser History Management (lines 157-161)
if ($pushUrl) {
(new Htmx())
->pushUrlHeader($pushUrl)
->applyTo($form);
}
Key Techniques Demonstrated
- Cascading dependent form fields
- Out-of-band (OOB) swaps for multiple simultaneous updates
- Browser history push to update URL as selections change
- Trigger detection to handle different field changes
- Wrapper selector patterns using
:has()pseudo-class - Progressive enhancement (form POSTs normally without JavaScript)
Common Mistakes
- Not using
:has()selector for wrapper targeting — Direct element ID might not exist on initial load - Forgetting to push URL — Users can't bookmark or share current state
- Not clearing dependent fields when parent changes — Old values persist incorrectly
- Missing OOB swap for related updates — Only primary target updates
See Also
- Previous: Production Patterns
- Next: Best Practices
- Reference: Dynamic Forms
- Reference:
/core/modules/system/tests/modules/test_htmx/