diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 4bbffc3..3c12cdd 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -209,6 +209,32 @@ 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 = ['#children']; + } + else { + $children = Element::children($elements); + } + + if (empty($children)) { + return ''; + } + + // Avoid making this change for the referenced elements variable because + // it could cause unexpected behaviour with other templating engines than + // Twig. Save the original referenced variable to allow making changes for + // the referenced variable in case explicitly wanted so. + $original_elements = &$elements; + $new_elements = array_intersect_key($elements, array_flip($children)); + $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 +454,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 +464,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 +491,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 +535,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); + // #attached and #markup values should be always saved to the referenced + // elements variable. + if (isset($original_elements)) { + $original_elements['#markup'] = $elements['#markup']; + $original_elements['#attached'] = $elements['#attached']; + } + // 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..de3b1c5 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/src/Tests/FileFieldValidateTest.php @@ -71,8 +71,10 @@ function testFileMaxSize() { $field_name = strtolower($this->randomMachineName()); $this->createFileField($field_name, 'node', $type_name, array(), array('required' => '1')); - $small_file = $this->getTestFile('text', 131072); // 128KB. - $large_file = $this->getTestFile('text', 1310720); // 1.2MB + // 128KB. + $small_file = $this->getTestFile('text', 131072); + // 1.2MB. + $large_file = $this->getTestFile('text', 1310720); // Test uploading both a large and small file with different increments. $sizes = array( @@ -185,4 +187,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..bf6b4df 100644 --- a/core/modules/image/src/Tests/ImageFieldValidateTest.php +++ b/core/modules/image/src/Tests/ImageFieldValidateTest.php @@ -8,6 +8,7 @@ * @group image */ class ImageFieldValidateTest extends ImageFieldTestBase { + /** * Test min/max resolution settings. */ @@ -156,4 +157,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'); + + $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.'); + } + }