diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 4bbffc3..491e8d2 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -209,6 +209,36 @@ protected function doRender(&$elements, $is_root_call = FALSE) { return ''; } + // Render only the children if the internal #render_children property is + // set. + // @see \Drupal\Core\Theme\ThemeManager::render(). + if (isset($elements['#render_children'])) { + // A non-empty #children property takes precedence. This happens only if + // it has been manually set into the render array. + if (!empty($elements['#children'])) { + $children_keys = ['#children']; + } + else { + $children_keys = Element::children($elements); + + if (empty($children_keys)) { + return ''; + } + } + + // Remove all elements except the children because the main level has been + // already rendered when the #render_children is set and therefore they + // should not have any effect on the render children. + $new_elements = array_intersect_key($elements, array_flip($children_keys)); + // Create a new variable that references the render array that was passed + // in. This allows the markup and cache information to be attached after + // rendering the new elements array. + $original_elements = &$elements; + // Change $elements to reference $new_elements. This prevents + // unintentional changes to the render array that was passed in. + $elements = &$new_elements; + } + if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); @@ -428,10 +458,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // Call the element's #theme function if it is set. Then any children of the - // element have to be rendered there. If the internal #render_children - // property is set, do not call the #theme function to prevent infinite - // recursion. - if ($theme_is_implemented && !isset($elements['#render_children'])) { + // element have to be rendered there. + if ($theme_is_implemented) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the @@ -440,10 +468,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $theme_is_implemented = ($elements['#children'] !== FALSE); } - // If #theme is not implemented or #render_children is set and the element - // has an empty #children attribute, render the children now. This is the - // same process as Renderer::render() but is inlined for speed. - if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { + // If #theme is not implemented and the element has an empty #children + // attribute, render the children now. This is the same process as + // Renderer::render() but is inlined for speed. + if (!$theme_is_implemented && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } @@ -467,9 +495,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. - // If the internal #render_children property is set, do not call the - // #theme_wrappers function(s) to prevent infinite recursion. - if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { + if (isset($elements['#theme_wrappers'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute @@ -513,6 +539,12 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); + // #markup should be always saved to the referenced elements variable to + // prevent re-rendering. + if (isset($original_elements)) { + $original_elements['#markup'] = $elements['#markup']; + } + // We've rendered this element (and its subtree!), now update the context. $context->update($elements); diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php index 9d43060..c21ace9 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/src/Tests/FileFieldValidateTest.php @@ -185,4 +185,25 @@ public function testFileRemoval() { $this->assertText('Article ' . $node->getTitle() . ' has been updated.'); } + /** + * Test the validation message is displayed only once for ajax uploads. + */ + public function testAJAXValidationMessage() { + $field_name = strtolower($this->randomMachineName()); + $this->createFileField($field_name, 'node', 'article'); + + $this->drupalGet('node/add/article'); + /** @var \Drupal\file\FileInterface $image_file */ + $image_file = $this->getTestFile('image'); + $edit = array( + 'files[' . $field_name . '_0]' => $this->container->get('file_system')->realpath($image_file->getFileUri()), + 'title[0][value]' => $this->randomMachineName(), + ); + $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_0_upload_button'); + $elements = $this->xpath('//div[contains(@class, :class)]', array( + ':class' => 'messages--error', + )); + $this->assertEqual(count($elements), 1, 'Ajax validation messages are displayed once.'); + } + } diff --git a/core/modules/image/src/Tests/ImageFieldValidateTest.php b/core/modules/image/src/Tests/ImageFieldValidateTest.php index 5f0bf21..0b7f9af 100644 --- a/core/modules/image/src/Tests/ImageFieldValidateTest.php +++ b/core/modules/image/src/Tests/ImageFieldValidateTest.php @@ -156,4 +156,26 @@ protected function getFieldSettings($min_resolution, $max_resolution) { ]; } + /** + * Test the validation message is displayed only once for ajax uploads. + */ + public function testAJAXValidationMessage() { + $field_name = strtolower($this->randomMachineName()); + $this->createImageField($field_name, 'article', ['cardinality' => -1]); + + $this->drupalGet('node/add/article'); + /** @var \Drupal\file\FileInterface[] $text_files */ + $text_files = $this->drupalGetTestFiles('text'); + $text_file = reset($text_files); + $edit = array( + 'files[' . $field_name . '_0][]' => $this->container->get('file_system')->realpath($text_file->uri), + 'title[0][value]' => $this->randomMachineName(), + ); + $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_0_upload_button'); + $elements = $this->xpath('//div[contains(@class, :class)]', array( + ':class' => 'messages--error', + )); + $this->assertEqual(count($elements), 1, 'Ajax validation messages are displayed once.'); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php b/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php index 8860c19..5fa8faf 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Render; +use Drupal\Core\Cache\Cache; use Drupal\KernelTests\KernelTestBase; /** @@ -16,7 +17,7 @@ class RenderTest extends KernelTestBase { * * @var array */ - public static $modules = array('system', 'common_test'); + public static $modules = array('system', 'common_test', 'theme_test'); /** * Tests theme preprocess functions being able to attach assets. @@ -44,6 +45,23 @@ function testDrupalRenderThemePreprocessAttached() { } /** + * Ensures that render array children are processed correctly. + */ + public function testRenderChildren() { + // Ensure that #prefix and #suffix is only being printed once since that is + // the behaviour the caller code expects. + $build = [ + '#type' => 'container', + '#theme' => 'theme_test_render_element_children', + '#prefix' => 'kangaroo', + '#suffix' => 'kitten', + ]; + $this->render($build); + $this->removeWhiteSpace(); + $this->assertNoRaw('