Loading core/misc/htmx/htmx-assets.js +6 −1 Changes for core/misc/htmx/htmx-assets.js: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -84,6 +84,9 @@ const { ctx } = detail; const url = new URL(ctx.request.action, document.location.href); const drupalIdentifier = (element) => { if (!(element instanceof Element)) { return ''; } if (element?.name) { return `${element.tagName.toLowerCase()}[name="${encodeURI(element.name)}"]`; } Loading Loading @@ -163,9 +166,11 @@ if ( task.swapSpec.style === 'none' || task.swapSpec.style.includes('after') || task.swapSpec.style.includes('before') task.swapSpec.style.includes('before') || !(task.target instanceof Element) ) { // Swap style does not remove elements // or target is not an Element, so no processing is required. continue; } processTargets.add(behaviorTarget(task.target)); Loading core/modules/system/tests/modules/test_htmx/src/Controller/HtmxFragmentController.php 0 → 100644 +70 −0 Changes for core/modules/system/tests/modules/test_htmx/src/Controller/HtmxFragmentController.php: 70 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\test_htmx\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Routing\Attribute\Route; /** * Returns responses for HTMX Test Fixtures routes. */ #[Route( name: 'test_htmx.', requirements: ['_permission' => 'access content'], defaults: ['_title' => 'Htmx Fragment'], )] final class HtmxFragmentController extends ControllerBase { /** * Builds the partial response. */ #[Route( path: '/test-htmx/htmx-fragment', name: 'htmx_fragment', defaults: [ 'target' => '[data-drupal-wrapper-selector="edit-partial-replace"]', 'swap' => 'outerHTML', ] )] #[Route( path: '/test-htmx/htmx-fragment-delete', name: 'htmx_fragment_delete', defaults: [ 'target' => '[data-drupal-wrapper-selector="edit-partial-replace"]', 'swap' => 'delete', ] )] public function buildPartial(string $target, string $swap): array { return [ '#type' => 'inline_template', '#template' => <<<HTMX_PARTIAL <hx-partial hx-target="{{ target }}" hx-swap="{{ swap }}"> {{item}} </hx-partial> HTMX_PARTIAL, '#context' => [ 'target' => $target, 'swap' => $swap, 'item' => $swap === 'delete' ? '' : [ '#type' => 'html_tag', '#tag' => 'p', '#value' => 'Inserted by htmx!', ], ], ]; } #[Route( path: '/test-htmx/htmx-fragment-missing', name: 'htmx_fragment_missing', )] public function buildDeleteInsertPartial(): array { return [ 'missing' => $this->buildPartial('div.does-not-exist', 'outerHTML'), 'insert' => $this->buildPartial('[data-drupal-wrapper-selector="edit-partial-replace"]', 'outerHTML'), ]; } } core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestForm.php +43 −0 Changes for core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestForm.php: 43 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -67,6 +67,49 @@ public function buildForm(array $form, FormStateInterface $form_state, ?string $ ->swapOob(TRUE) ->applyTo($form['data'], '#wrapper_attributes'); $form['bad_swap_target'] = [ '#type' => 'checkbox', '#title' => 'Target does not exist', ]; (new Htmx()) ->get($formUrl) ->select('select') ->swap('innerHTML') ->target('div.bad-target') ->applyTo($form['bad_swap_target']); // Test hx-replace fragments. $partialReplace = Url::fromRoute('test_htmx.htmx_fragment'); $form['partial-replace'] = [ '#type' => 'item', '#title' => 'Replacement target', ]; $form['partial'] = [ '#type' => 'checkbox', '#title' => 'Replace via partial', ]; (new Htmx()) ->get($partialReplace) ->onlyMainContent() ->applyTo($form['partial']); $partialMissingTarget = Url::fromRoute('test_htmx.htmx_fragment_missing'); $form['partial_missing'] = [ '#type' => 'checkbox', '#title' => 'Target does not exist', ]; (new Htmx()) ->get($partialMissingTarget) ->onlyMainContent() ->applyTo($form['partial_missing']); $partialDelete = Url::fromRoute('test_htmx.htmx_fragment_delete'); $form['partial_delete'] = [ '#type' => 'checkbox', '#title' => 'Delete via partial', ]; (new Htmx()) ->get($partialDelete) ->onlyMainContent() ->applyTo($form['partial_delete']); $push = FALSE; if ($this->getHtmxTriggerName() === 'type') { $form['data']['#markup'] = ''; Loading core/tests/Drupal/FunctionalJavascriptTests/Core/Htmx/HtmxDynamicFormTest.php +70 −2 Changes for core/tests/Drupal/FunctionalJavascriptTests/Core/Htmx/HtmxDynamicFormTest.php: 70 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -36,7 +36,7 @@ public function testHtmxForm(): void { 'access content', ])); // Check that the data is empty on load. $this->drupalGet('/htmx-test-attachments/form-builder-test/'); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $page = $this->getSession()->getPage(); $this->assertTrue($this->assertSession()->optionExists('selected', '- None -')->isSelected()); $this->assertSession()->pageTextNotContains('Data is'); Loading Loading @@ -91,7 +91,7 @@ public function testWrapperFormat(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('/htmx-test-attachments/wrapper'); $this->drupalGet('htmx-test-attachments/wrapper'); $button = $this->assertSession()->elementExists('css', '[name="replace"]'); // Should be present in the HTMX markup on the button. $this->assertSession()->elementExists('css', '[data-hx-drupal-only-main-content]'); Loading @@ -102,4 +102,72 @@ public function testWrapperFormat(): void { $this->assertSession()->elementExists('css', '.htmx-test-flag'); } /** * Verify that a bad swap target selector does not cause an error. */ public function testMissingSwapTarget(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $checkbox = $this->assertSession()->elementExists('css', '[name="bad_swap_target"]'); // Selector used in the htmx target attribute. $this->assertSession()->elementNotExists('css', 'div.bad-target'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); } /** * Verify that response with two partials: one bad, one good. * * The first partial is missing a target, the second is good. The missing * target should not cause an error, execution should continue, and the good * partial should be applied. */ public function testMissingPartialTarget(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $checkbox = $this->assertSession()->elementExists('css', '[name="partial_missing"]'); // Selector used in the htmx target attribute. $this->assertSession()->elementNotExists('css', 'div.does-not-exist'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->elementNotExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $this->assertSession()->pageTextContainsOnce('Inserted by htmx!'); } /** * Verify that an hx-partial response replaces the target. */ public function testPartialTarget(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $this->assertSession()->elementExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $this->assertSession()->pageTextNotContains('Inserted by htmx!'); $checkbox = $this->assertSession()->elementExists('css', '[name="partial"]'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->elementNotExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $this->assertSession()->pageTextContainsOnce('Inserted by htmx!'); } /** * Verify that an hx-partial response with a delete swap removes the target. */ public function testPartialDelete(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $this->assertSession()->elementExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $checkbox = $this->assertSession()->elementExists('css', '[name="partial_delete"]'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->elementNotExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); } } Loading
core/misc/htmx/htmx-assets.js +6 −1 Changes for core/misc/htmx/htmx-assets.js: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -84,6 +84,9 @@ const { ctx } = detail; const url = new URL(ctx.request.action, document.location.href); const drupalIdentifier = (element) => { if (!(element instanceof Element)) { return ''; } if (element?.name) { return `${element.tagName.toLowerCase()}[name="${encodeURI(element.name)}"]`; } Loading Loading @@ -163,9 +166,11 @@ if ( task.swapSpec.style === 'none' || task.swapSpec.style.includes('after') || task.swapSpec.style.includes('before') task.swapSpec.style.includes('before') || !(task.target instanceof Element) ) { // Swap style does not remove elements // or target is not an Element, so no processing is required. continue; } processTargets.add(behaviorTarget(task.target)); Loading
core/modules/system/tests/modules/test_htmx/src/Controller/HtmxFragmentController.php 0 → 100644 +70 −0 Changes for core/modules/system/tests/modules/test_htmx/src/Controller/HtmxFragmentController.php: 70 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\test_htmx\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Routing\Attribute\Route; /** * Returns responses for HTMX Test Fixtures routes. */ #[Route( name: 'test_htmx.', requirements: ['_permission' => 'access content'], defaults: ['_title' => 'Htmx Fragment'], )] final class HtmxFragmentController extends ControllerBase { /** * Builds the partial response. */ #[Route( path: '/test-htmx/htmx-fragment', name: 'htmx_fragment', defaults: [ 'target' => '[data-drupal-wrapper-selector="edit-partial-replace"]', 'swap' => 'outerHTML', ] )] #[Route( path: '/test-htmx/htmx-fragment-delete', name: 'htmx_fragment_delete', defaults: [ 'target' => '[data-drupal-wrapper-selector="edit-partial-replace"]', 'swap' => 'delete', ] )] public function buildPartial(string $target, string $swap): array { return [ '#type' => 'inline_template', '#template' => <<<HTMX_PARTIAL <hx-partial hx-target="{{ target }}" hx-swap="{{ swap }}"> {{item}} </hx-partial> HTMX_PARTIAL, '#context' => [ 'target' => $target, 'swap' => $swap, 'item' => $swap === 'delete' ? '' : [ '#type' => 'html_tag', '#tag' => 'p', '#value' => 'Inserted by htmx!', ], ], ]; } #[Route( path: '/test-htmx/htmx-fragment-missing', name: 'htmx_fragment_missing', )] public function buildDeleteInsertPartial(): array { return [ 'missing' => $this->buildPartial('div.does-not-exist', 'outerHTML'), 'insert' => $this->buildPartial('[data-drupal-wrapper-selector="edit-partial-replace"]', 'outerHTML'), ]; } }
core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestForm.php +43 −0 Changes for core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestForm.php: 43 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -67,6 +67,49 @@ public function buildForm(array $form, FormStateInterface $form_state, ?string $ ->swapOob(TRUE) ->applyTo($form['data'], '#wrapper_attributes'); $form['bad_swap_target'] = [ '#type' => 'checkbox', '#title' => 'Target does not exist', ]; (new Htmx()) ->get($formUrl) ->select('select') ->swap('innerHTML') ->target('div.bad-target') ->applyTo($form['bad_swap_target']); // Test hx-replace fragments. $partialReplace = Url::fromRoute('test_htmx.htmx_fragment'); $form['partial-replace'] = [ '#type' => 'item', '#title' => 'Replacement target', ]; $form['partial'] = [ '#type' => 'checkbox', '#title' => 'Replace via partial', ]; (new Htmx()) ->get($partialReplace) ->onlyMainContent() ->applyTo($form['partial']); $partialMissingTarget = Url::fromRoute('test_htmx.htmx_fragment_missing'); $form['partial_missing'] = [ '#type' => 'checkbox', '#title' => 'Target does not exist', ]; (new Htmx()) ->get($partialMissingTarget) ->onlyMainContent() ->applyTo($form['partial_missing']); $partialDelete = Url::fromRoute('test_htmx.htmx_fragment_delete'); $form['partial_delete'] = [ '#type' => 'checkbox', '#title' => 'Delete via partial', ]; (new Htmx()) ->get($partialDelete) ->onlyMainContent() ->applyTo($form['partial_delete']); $push = FALSE; if ($this->getHtmxTriggerName() === 'type') { $form['data']['#markup'] = ''; Loading
core/tests/Drupal/FunctionalJavascriptTests/Core/Htmx/HtmxDynamicFormTest.php +70 −2 Changes for core/tests/Drupal/FunctionalJavascriptTests/Core/Htmx/HtmxDynamicFormTest.php: 70 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -36,7 +36,7 @@ public function testHtmxForm(): void { 'access content', ])); // Check that the data is empty on load. $this->drupalGet('/htmx-test-attachments/form-builder-test/'); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $page = $this->getSession()->getPage(); $this->assertTrue($this->assertSession()->optionExists('selected', '- None -')->isSelected()); $this->assertSession()->pageTextNotContains('Data is'); Loading Loading @@ -91,7 +91,7 @@ public function testWrapperFormat(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('/htmx-test-attachments/wrapper'); $this->drupalGet('htmx-test-attachments/wrapper'); $button = $this->assertSession()->elementExists('css', '[name="replace"]'); // Should be present in the HTMX markup on the button. $this->assertSession()->elementExists('css', '[data-hx-drupal-only-main-content]'); Loading @@ -102,4 +102,72 @@ public function testWrapperFormat(): void { $this->assertSession()->elementExists('css', '.htmx-test-flag'); } /** * Verify that a bad swap target selector does not cause an error. */ public function testMissingSwapTarget(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $checkbox = $this->assertSession()->elementExists('css', '[name="bad_swap_target"]'); // Selector used in the htmx target attribute. $this->assertSession()->elementNotExists('css', 'div.bad-target'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); } /** * Verify that response with two partials: one bad, one good. * * The first partial is missing a target, the second is good. The missing * target should not cause an error, execution should continue, and the good * partial should be applied. */ public function testMissingPartialTarget(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $checkbox = $this->assertSession()->elementExists('css', '[name="partial_missing"]'); // Selector used in the htmx target attribute. $this->assertSession()->elementNotExists('css', 'div.does-not-exist'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->elementNotExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $this->assertSession()->pageTextContainsOnce('Inserted by htmx!'); } /** * Verify that an hx-partial response replaces the target. */ public function testPartialTarget(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $this->assertSession()->elementExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $this->assertSession()->pageTextNotContains('Inserted by htmx!'); $checkbox = $this->assertSession()->elementExists('css', '[name="partial"]'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->elementNotExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $this->assertSession()->pageTextContainsOnce('Inserted by htmx!'); } /** * Verify that an hx-partial response with a delete swap removes the target. */ public function testPartialDelete(): void { $this->drupalLogin($this->drupalCreateUser([ 'access content', ])); $this->drupalGet('htmx-test-attachments/form-builder-test/'); $this->assertSession()->elementExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); $checkbox = $this->assertSession()->elementExists('css', '[name="partial_delete"]'); $checkbox->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->elementNotExists('css', '[data-drupal-wrapper-selector="edit-partial-replace"]'); } }