Skip to content

Production Example: ConfigSingleExportForm

When to Use

Reference this when you want a complete, production-ready HTMX implementation demonstrating cascading selects, OOB updates, and history management — all from Drupal core.

Decision

Technique Where used Key method
Cascading selects type → name select('*:has(>select[name="name"])')
OOB update Clear export on type change swapOob('outerHTML:[data-export-wrapper]')
Trigger detection Route different field changes getHtmxTriggerName()
History push Update URL as selections change pushUrlHeader($url)

Reference: /core/modules/config/src/Form/ConfigSingleExportForm.php

Pattern

1. First select updates second select (lines 92–107):

(new Htmx())
  ->post($form_url)
  ->onlyMainContent()
  ->select('*:has(>select[name="config_name"])')
  ->target('*:has(>select[name="config_name"])')
  ->swap('outerHTML')
  ->applyTo($form['config_type']);

2. Second select updates display region (lines 117–125):

(new Htmx())
  ->post($form_url)
  ->onlyMainContent()
  ->select('[data-export-wrapper]')
  ->target('[data-export-wrapper]')
  ->swap('outerHTML')
  ->applyTo($form['config_name']);

3. Trigger detection + OOB clear + history push (lines 136–161):

$trigger = $this->getHtmxTriggerName();

if ($trigger == 'config_type') {
  (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') {
  $form['export'] = $this->updateExport($form, $default_type, $default_name);
  $pushUrl = Url::fromRoute('config.export_single',
    ['config_type' => $default_type, 'config_name' => $default_name]);
}

if ($pushUrl) {
  (new Htmx())->pushUrlHeader($pushUrl)->applyTo($form);
}

Key techniques demonstrated:

  • Cascading dependent form fields
  • 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

  • Wrong: Not using :has() selector for wrapper targeting → Right: Direct element ID might not exist on initial load
  • Wrong: Forgetting to push URL → Right: Users can't bookmark or share current state
  • Wrong: Not clearing dependent fields when parent changes → Right: Old values persist incorrectly
  • Wrong: Missing OOB swap for related updates → Right: Only primary target updates

See Also