diff --git a/modules/ui_patterns_layouts/tests/fixtures/config/core.entity_view_display.node.page.full.yml b/modules/ui_patterns_layouts/tests/fixtures/config/core.entity_view_display.node.page.full.yml index 895307ce901e32337b647504089619e84418ae0c..a6e2e58a50cf03a4db99e93f93892138a4fde091 100644 --- a/modules/ui_patterns_layouts/tests/fixtures/config/core.entity_view_display.node.page.full.yml +++ b/modules/ui_patterns_layouts/tests/fixtures/config/core.entity_view_display.node.page.full.yml @@ -23,3 +23,7 @@ bundle: page mode: full hidden: layout_builder__layout: true + field_text_1: true + title: true + body: true + links: true diff --git a/modules/ui_patterns_layouts/tests/src/Functional/LayoutBuilderRenderTest.php b/modules/ui_patterns_layouts/tests/src/Functional/LayoutBuilderRenderTest.php index 05ab30b919ae657fe73ed008f373a783131632d7..a1afc5dec38a1d1b62747538f3285cee75cc643a 100644 --- a/modules/ui_patterns_layouts/tests/src/Functional/LayoutBuilderRenderTest.php +++ b/modules/ui_patterns_layouts/tests/src/Functional/LayoutBuilderRenderTest.php @@ -112,4 +112,32 @@ class LayoutBuilderRenderTest extends UiPatternsFunctionalTestBase { } + /** + * Tests preview and output of slots. + */ + public function testRenderEmptySlots(): void { + $node = $this->createTestContentNode('page', ['field_text_1' => ['value' => '']]); + $config_import = $this->loadConfigFixture(__DIR__ . '/../../fixtures/config/core.entity_view_display.node.page.full.yml'); + $this->importConfigFixture( + 'core.entity_view_display.node.page.full', + $config_import + ); + + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->drupalGet('admin/structure/types/manage/page/display/full/layout'); + $assert_session->elementExists('css', '.ui-patterns-test-component'); + + // Add body to slot and check for existence. + $page->clickLink('Add block'); + $page->clickLink('Text (formatted)'); + $page->pressButton('Add block'); + $page->pressButton('Save layout'); + $this->drupalGet('node/' . $node->id()); + + $assert_session->elementExists('css', '.ui-patterns-test-component'); + $assert_session->elementTextContains('css', '.ui-patterns-slots-empty-slot', 'EMPTY SLOT'); + + } + } diff --git a/src/Element/ComponentElementAlter.php b/src/Element/ComponentElementAlter.php index fddd816b86f7bed4ff69ea889a23dcc1218f646b..1219b2c8bdd0a5b62ca186ab4e4d3e499a3b5885 100644 --- a/src/Element/ComponentElementAlter.php +++ b/src/Element/ComponentElementAlter.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Drupal\ui_patterns\Element; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Render\Element; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\ComponentPluginManager; use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType; @@ -44,14 +46,18 @@ class ComponentElementAlter implements TrustedCallbackInterface { * Normalize slots. */ public function normalizeSlots(array $element): array { + foreach ($element["#slots"] as $slot_id => $slot) { // Because SDC validator is sometimes confused by a null slot. if (is_null($slot)) { unset($element['#slots'][$slot_id]); continue; } + // Because SDC validator is sometimes confused by an empty slot. - if (is_array($slot) && empty($slot)) { + // We check the current slot render element. + if (self::isSlotEmpty($slot)) { + self::mergeSlotBubbleableMetadata($element, $slot, 1); unset($element['#slots'][$slot_id]); continue; } @@ -89,4 +95,53 @@ class ComponentElementAlter implements TrustedCallbackInterface { return $element; } + /** + * Merge slots metadata to the component recursive. + */ + protected static function mergeSlotBubbleableMetadata(array &$element, array $slot, int $max_level = 1, int $level = 0): void { + if ($level < $max_level) { + foreach (Element::getVisibleChildren($slot) as $child) { + self::mergeSlotBubbleableMetadata($element, $slot[$child], $max_level, $level + 1); + } + } + $elementMetadata = BubbleableMetadata::createFromRenderArray($element); + $elementMetadata->merge(BubbleableMetadata::createFromRenderArray($slot)); + $elementMetadata->applyTo($element); + } + + /** + * Checks the given render element for emptiness. + * + * The method calls Element::isEmpty recursive until max level is reached. + * + * @param array $slot + * The render array. + * @param int $max_level + * The level of recursion. + * @param int $level + * Internal used level. + * + * @return bool + * Returns true for empty. + */ + protected static function isSlotEmpty(array $slot, int $max_level = 1, int $level = 0): bool { + if (is_array($slot) && empty($slot)) { + return TRUE; + } + if ($level < $max_level) { + foreach (Element::getVisibleChildren($slot) as $child) { + if (self::isSlotEmpty($slot[$child], $max_level, $level + 1) === FALSE) { + return FALSE; + } + else { + unset($slot[$child]); + } + } + } + if (Element::isEmpty($slot)) { + return TRUE; + } + return FALSE; + } + } diff --git a/src/Element/ComponentElementBuilder.php b/src/Element/ComponentElementBuilder.php index 488181a4cac16ec62e4efaddd3ecf12c5bd2fee2..a4c629bb46454b5cd79efe4689e357c2b637e2fe 100644 --- a/src/Element/ComponentElementBuilder.php +++ b/src/Element/ComponentElementBuilder.php @@ -181,6 +181,7 @@ class ComponentElementBuilder implements TrustedCallbackInterface { if ($this->isSingletonRenderArray($build["#slots"][$slot_id])) { $build["#slots"][$slot_id] = $build["#slots"][$slot_id][0]; } + return $build; } diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.twig b/tests/modules/ui_patterns_test/components/test-component/test-component.twig index 847fc7439e99d9c7378e8e88087fd76995bcb865..0870e69797100f48ec5265fac5080379969dcaa3 100644 --- a/tests/modules/ui_patterns_test/components/test-component/test-component.twig +++ b/tests/modules/ui_patterns_test/components/test-component/test-component.twig @@ -68,4 +68,9 @@ <div class="ui-patterns-slots-slot"> {{ slot }} </div> + <div class="ui-patterns-slots-empty-slot"> + {% if slot is empty %} + EMPTY SLOT + {% endif %} + </div> </div>