diff --git a/render_example/css/render_example.css b/render_example/css/render_example.css new file mode 100644 index 0000000..0da7f30 --- /dev/null +++ b/render_example/css/render_example.css @@ -0,0 +1,20 @@ +.render-array { + border: 2px solid #000; + margin-top: 10px; + padding-left: 5px; + padding-top: 5px; +} + +.render-header { + font-size: large; + font-style: italic; +} + +.unrendered-label { + font-style: italic; + margin-top: 10px; +} + +.rendered { + background-color: #add8e6; +} diff --git a/render_example/render_example.info.yml b/render_example/render_example.info.yml new file mode 100644 index 0000000..eac2c56 --- /dev/null +++ b/render_example/render_example.info.yml @@ -0,0 +1,12 @@ +name: Render example +description: "Demonstrates drupal_render's capabilities and altering render arrays." +type: module +package: Example modules +core: 8.x +dependencies: + - devel + - kint + - examples +stylesheets: + all: + - css/render_example.css diff --git a/render_example/render_example.module b/render_example/render_example.module new file mode 100644 index 0000000..4ece3d4 --- /dev/null +++ b/render_example/render_example.module @@ -0,0 +1,246 @@ + array( + 'render element' => 'element', + ), + 'render_example_add_notes' => array( + 'render element' => 'element', + ), + 'render_array' => array( + 'render element' => 'element', + ), + 'render_example_aggregate' => array( + 'render element' => 'element', + ), + ); + + return $items; +} + +/** + * A '#post_render' function to add a little markup onto the end markup. + * + * @param $markup + * The rendered element. + * @param $element + * The element which was rendered (for reference) + * + * @return + * Markup altered as necessary. In this case we add a little postscript to it. + */ +function render_example_add_prefix($markup, $element) { + $markup = '
This markup was added after rendering by a #post_render
' . $markup; + return $markup; +} + +/** + * A '#pre_render' function. + * + * @param $element + * The element which will be rendered. + * + * @return + * The altered element. In this case we add a #prefix to it. + */ +function render_example_add_suffix($element) { + $element['#suffix'] = '
' . t('This #suffix was added by a #pre_render') . '
'; + + return $element; +} + +/** + * A '#pre_render' function. + * + * @param $element + * The element which will be rendered. + * + * @return + * The altered element. In this case we add the #markup. + */ +function render_example_cache_pre_render($element) { + $element['#markup'] = render_example_cache_expensive(); + + return $element; +} + +/** + * A potentially expensive function. + * + * @return + * Some demo text. + */ +function render_example_cache_expensive() { + $interval = 60; + $time_message = t('The current time was %time when this was cached. Updated every %interval seconds', array( + '%time' => date('r'), + '%interval' => $interval + )); + // Uncomment the following line to demonstrate that this function is not + // being run when the rendered array is cached. + // drupal_set_message($time_message); + return $time_message; +} + +/*************** Altering Section ************************** + * The following section of the example builds and arranges the altering + * example. + */ + +/** + * Implements hook_preprocess_page(). + * + * + */ +function render_example_preprocess_page(&$variables) { + $config = \Drupal::config('render_example.settings'); + $page = &$variables['page']; + + // Add a list of items to the top of sidebar_first. + // This shows how #theme and #theme_wrappers work. + if ($config->get('render_example.note_about_render_arrays') && !empty($page['sidebar_first'])) { + $page['sidebar_first']['render_array_note'] = array( + '#title' => t('Render Array Example'), + '#items' => array( + t('Render arrays are everywhere in D7.'), + t('Leave content unrendered as much as possible.'), + t('This allows rearrangement and alteration very late in page cycle.'), + ), + // The functions in #pre_render get to alter the actual data before it + // gets rendered by the various theme functions. + '#pre_render' => array('render_example_change_to_ol'), + // The functions in #post_render get both the element and the rendered + // data and can add to the rendered data. + '#post_render' => array('render_example_add_hr'), + // The #theme theme operation gets the first chance at rendering the + // element and its children. + '#theme' => 'item_list', + // Then the theme operations in #theme_wrappers can wrap more around + // what #theme left in #chilren. + '#theme_wrappers' => array( + 'render_example_add_div', + 'render_example_add_notes' + ), + '#weight' => -9999, + ); + + // Force the sidebar to be re-sorted. + $page['sidebar_first']['#sorted'] = FALSE; + } + + // Move the breadcrumbs into the content area. + if ($config->get('render_example.move_breadcrumbs') && !empty($page['breadcrumb']) && !empty($page['content'])) { + $page['content']['breadcrumb'] = $page['breadcrumb']; + unset($page['breadcrumb']); + $page['content']['breadcrumb']['#weight'] = -99999; + + // Force the content to be re-sorted. + $page['content']['#sorted'] = FALSE; + } + + // Re-sort the sidebar in reverse order. + if ($config->get('render_example.reverse_sidebar') && !empty($page['sidebar_first'])) { + $page['sidebar_first'] = array_reverse($page['sidebar_first']); + foreach (\Drupal\Core\Render\Element::children($page['sidebar_first']) as $element) { + // Reverse the weights if they exist. + if (!empty($page['sidebar_first'][$element]['#weight'])) { + $page['sidebar_first'][$element]['#weight'] *= -1; + } + } + // This forces the sidebar to be re-sorted. + $page['sidebar_first']['#sorted'] = FALSE; + } + + // Show the render array used to build the page render array display. + if ($config->get('render_example.show_page')) { + $page['content']['page_render_array'] = array( + '#type' => 'markup', + '#prefix' => '

'. t('The page render array') . '

', + '#markup' => kpr($page, TRUE), + '#weight' => -99999, + ); + + $page['content']['#sorted'] = FALSE; + } +} + +/** + * Implements hook_preprocess_block(). + */ +function render_example_preprocess_block(&$variables){ + $config = \Drupal::config('render_example.settings'); + + // Add #prefix and #suffix to a block to wrap a div around it. + if ($config->get('render_example.prefix')) { + $variables['content']['#prefix'] = '

' . t('Prefixed') . '

'; + $variables['content']['#suffix'] = '' . t('Block suffix') . '
'; + } + + // Show the render array used to build each block. + if ($config->get('render_example.show_block')) { + $variables['content']['block_render_array'] = array( + '#type' => 'markup', + '#prefix' => '

'. t('The block render array for ') . $variables['plugin_id'] . '

', + '#markup' => kpr($variables, TRUE), + ); + } +} + +/** + * Adds a #type to the element before it gets rendered. + * In this case, changes from the default 'ul' to 'ol'. + * + * @param $element + * The element to be altered, in this case a list, ready for theme_item_list. + * + * @return + * The altered list (with '#type') + */ +function render_example_change_to_ol($element) { + $element['#type'] = 'ol'; + return $element; +} + +/** + * This #post_render function gets to alter the rendered output after all + * theme functions have acted on it, and it receives the original data, so + * can make decisions based on that. In this example, no use is made of the + * passed-in $element. + * + * @param $markup + * The already-rendered data + * @param unknown_type $element + * The data element that was rendered + * + * @return + * The altered data. + */ +function render_example_add_hr($markup, $element) { + $output = $markup . '
'; + return $output; +} +/** + * @} End of "defgroup render_example". + */ diff --git a/render_example/render_example.routing.yml b/render_example/render_example.routing.yml new file mode 100644 index 0000000..d87673b --- /dev/null +++ b/render_example/render_example.routing.yml @@ -0,0 +1,21 @@ +render_example.description: + path: 'examples/render_example' + defaults: + _controller: '\Drupal\render_example\Controller\RenderExampleController::description' + requirements: + _access: 'TRUE' + +render_example.altering: + path: 'examples/render_example/altering' + defaults: + _form: '\Drupal\render_example\Form\RenderExampleDemoForm' + _title: 'Alter pages and blocks' + requirements: + _access: 'TRUE' + +render_example.arrays: + path: 'examples/render_example/arrays' + defaults: + _controller: '\Drupal\render_example\Controller\RenderExampleController::arrays' + requirements: + _access: 'TRUE' diff --git a/render_example/src/Controller/RenderExampleController.php b/render_example/src/Controller/RenderExampleController.php new file mode 100644 index 0000000..93f54c6 --- /dev/null +++ b/render_example/src/Controller/RenderExampleController.php @@ -0,0 +1,201 @@ + array( + '#type' => 'markup', + /** + * Basic render arrays are the markup type which gets rendered + * directly to the page. + */ + '#markup' => t('The render example provides examples around creating and altering render arrays.'), + ), + 'links' => array( + '#items' => array( + /** + * The l() function takes a string and a Url object as arguments. + * + * The t() function processes a string and makes sure that it is + * translated if necessary. + * + * The Url::fromRoute() function creates a Url object from a route + * which the l() function uses to obtain the Url path. + */ + \Drupal::l( + t('Demonstration of of render array usage.'), + \Drupal\Core\Url::fromRoute('render_example.arrays') + ), + \Drupal::l( + t('Using hooks to change various components.'), + \Drupal\Core\Url::fromRoute('render_example.altering') + ), + ), + /** + * If other types of render arrays are used, you can either specify + * the type or the theme and this will be used to process the render + * array into markup that is placed on the page. + */ + '#theme' => 'item_list', + ), + ); + + return $output; + } + + /** + * This produces a render array as a page. + * + * This page provides example usage for render arrays. This page path was + * defined in + * render_example.routing.yml + */ + public function arrays() { + $examples = array( + /** + * A render array can contain a list of render arrays and each will be + * gathered up and displayed on the page. + */ + // Demonstrate the simplest markup, a #markup element. + 'super simple #markup' => array( + '#description' => t('Super simple #markup'), + '#markup' => '

' . t('Some basic text in a #markup (shows basic markup and how it is rendered)' . '

'), + ), + // Shows how #prefix and #suffix can add markup into an array. + 'Using #prefix and #suffix' => array( + '#description' => t('Using #prefix and #suffix'), + '#type' => 'markup', + '#markup' => t('This one adds a prefix and suffix, which put a div around the item'), + '#prefix' => '
', + '#suffix' => '
', + ), + // When #theme is provided, it is the #theme function's job to figure out + // the meaning of the render array. The #theme function receives the entire + // element in $variables and must return it, where it will be the content + // of '#children'. When a #theme or other function is provided, custom + // properties can be invented and used as needed, as the #separator + // property provided here. + // + // If #theme is not provided, either explicitly or by the underlying + // element, then the children are rendered using their own properties and + // the results go into #children. + '#theme for an element' => array( + '#description' => t('Theme for an element'), + 'child' => array( + t('This is some text that should be put together'), + t('This is some more text that we need'), + ), + '#separator' => ' | ', // Made up for this theme function. + '#theme' => 'render_example_aggregate', + ), + // #theme_wrappers provides an array of theme functions which theme the + // envelope or "wrapper" of a set of child elements. The theme function + // finds its element children (the sub-arrays) already rendered in + // '#children'. + '#theme_wrappers demonstration' => array( + '#description' => t('theme_wrappers demonstration'), + 'child1' => array('#markup' => t('Markup for child1')), + 'child2' => array('#markup' => t('Markup for child2')), + '#theme_wrappers' => array( + 'render_example_add_div', + 'render_example_add_notes' + ), + ), + // Add '#pre_render' and '#post_render' handlers. + // - '#pre_render' functions get access to the array before it is rendered + // and can change it. This is similar to a theme function, but it is a + // specific fixed function and changes the array in place rather than + // rendering it.. + // - '#post_render' functions get access to the rendered content, but also + // have the original array available. + 'pre_render and post_render' => array( + '#description' => t('pre_render and post_render'), + '#markup' => '
' . t('markup for pre_render and post_render example') . '
', + '#pre_render' => array('render_example_add_suffix'), + '#post_render' => array('render_example_add_prefix'), + ), + // Cache an element for 60 seconds using #cache. + // The assumption here is that this is an expensive item to render, perhaps + // large or otherwise expensive. Of course here it's just a piece of markup, + // so we don't get the value. + // + // #cache allows us to set + // - 'keys', an array of strings that will create the string cache key. + // - 'bin', the cache bin + // - 'expire', the expire timestamp. Note that this is actually limited + // to the granularity of a cron run. + // - 'granularity', a bitmask determining at what level the caching is done + // (user, role, page). + '#cache demonstration' => array( + '#description' => t('#cache demonstration'), + // If your expensive function were to be executed here it would happen + // on every page load regardless of the cache. The actual markup is + // added via the #pre_render function, so that drupal_render() will only + // execute the expensive function if this array has not been cached. + '#markup' => '', + '#pre_render' => array('render_example_cache_pre_render'), + '#cache' => array( + 'keys' => array('render_example', 'cache', 'demonstration'), + 'bin' => 'cache', + 'expire' => time() + 60, + //'granularity' => DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE, + ), + ), + ); + + $output = array(); + /** + * We are going to create a new output render array that pairs each + * example with a set of helper render arrays. These are used to display + * the description as a title and the unrendered content alongside the + * examples. + */ + foreach ($examples as $key => &$item) { + $output[$key] = array( + 'description' => array( + '#markup' => $item['#description'], + ), + 'rendered' => $item, + 'unrendered' => array( + 'raw' => array( + '#type' => 'markup', + '#markup' => htmlentities(\Drupal\Component\Utility\Variable::export($item)), + ), + // The kpr() function is from devel module and is here only allow us + // to output the array in a way that's easy to explore. + 'kpr' => array( + '#type' => 'markup', + '#markup' => kpr($item, TRUE), + ), + '#theme' => 'render_array_help', + ), + '#theme' => 'render_array', + ); + } + + return $output; + } +} diff --git a/render_example/src/Form/RenderExampleDemoForm.php b/render_example/src/Form/RenderExampleDemoForm.php new file mode 100644 index 0000000..a8840f8 --- /dev/null +++ b/render_example/src/Form/RenderExampleDemoForm.php @@ -0,0 +1,110 @@ +config('render_example.settings'); + + $form['description'] = array( + '#type' => 'markup', + '#markup' => t('This example shows what render arrays look like in the building of a page. It will not work unless the user running it has the "access devel information" privilege. It shows both the actual arrays used to build a page or block and also the capabilities of altering the page late in its lifecycle.'), + ); + + $form['show_arrays'] = array( + '#type' => 'fieldset', + '#title' => t('Show render arrays'), + 'render_example_show_block' => array( + '#type' => 'checkbox', + '#title' => t('Show block render arrays'), + '#default_value' => $config->get('render_example.show_block'), + ), + 'render_example_show_page' => array( + '#type' => 'checkbox', + '#title' => t('Show page render arrays'), + '#default_value' => $config->get('render_example.show_page'), + ), + ); + + $form['page_fiddling'] = array( + '#type' => 'fieldset', + '#title' => t('Make changes on page via hook_page_alter()'), + 'render_example_note_about_render_arrays' => array( + '#title' => t('Add a note about render arrays to top of sidebar_first (if it exists)'), + '#description' => t('Creates a simple render array that displays the use of #pre_render, #post_render, #theme, and #theme_wrappers.'), + '#type' => 'checkbox', + '#default_value' => $config->get('render_example.note_about_render_arrays'), + ), + 'render_example_move_breadcrumbs' => array( + '#title' => t('Move the breadcrumbs to the top of the content area'), + '#description' => t('Uses hook_preprocess_page() to move the breadcrumbs into another region.'), + '#type' => 'checkbox', + '#default_value' => $config->get('render_example.move_breadcrumbs'), + ), + 'render_example_reverse_sidebar' => array( + '#title' => t('Reverse ordering of sidebar_first elements (if it exists) - will affect the above'), + '#description' => t('Uses hook_preprocess_page() to reverse the ordering of items in sidebar_first'), + '#type' => 'checkbox', + '#default_value' => $config->get('render_example.reverse_sidebar'), + ), + 'render_example_prefix' => array( + '#title' => t('Use #prefix and #suffix to wrap a div around every block'), + '#description' => t('Uses hook_block_view_alter() to wrap all blocks with a div using #prefix and #suffix'), + '#type' => 'checkbox', + '#default_value' => $config->get('render_example.prefix'), + ), + ); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['render_example.settings']; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + + $config = $this->config('render_example.settings'); + $config->set('render_example.show_block', $values['render_example_show_block'])->save(); + $config->set('render_example.show_page', $values['render_example_show_page'])->save(); + $config->set('render_example.note_about_render_arrays', $values['render_example_note_about_render_arrays'])->save(); + $config->set('render_example.move_breadcrumbs', $values['render_example_move_breadcrumbs'])->save(); + $config->set('render_example.reverse_sidebar', $values['render_example_reverse_sidebar'])->save(); + $config->set('render_example.prefix', $values['render_example_prefix'])->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/render_example/src/Tests/RenderExampleTest.php b/render_example/src/Tests/RenderExampleTest.php new file mode 100644 index 0000000..85551a9 --- /dev/null +++ b/render_example/src/Tests/RenderExampleTest.php @@ -0,0 +1,140 @@ +xpath($xpath); + $this->assertTrue(!empty($result), format_string('Found xpath %xpath', array('%xpath' => $xpath))); + } + } + + /** + * Asserts that the string value of the result is the same as the passed text. + * + * @param $xpath_array + * Array of keyed arrays of tests to be made. Each child array consists of + * $xpath => $expected_text + */ + protected function assertRenderedText($xpath_array) { + foreach ($xpath_array as $xpath => $text) { + $result = $this->xpath($xpath); + $this->assertTrue((string) $result[0][0] == $text, format_string('%ary selects text %text', array( + '%ary' => $xpath, + '%text' => $text + ))); + } + } + + /** + * Login user, create an example node, and test blog functionality through the admin and user interfaces. + */ + public function testRenderExampleBasic() { + // Create a user that can access devel information and log in. + $web_user = $this->drupalCreateUser(array( + 'access devel information', + 'access content' + )); + $this->drupalLogin($web_user); + + // Turn on the block render array display and make sure it shows up. + $edit = array( + 'render_example_show_block' => TRUE, + ); + $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration')); + + $xpath_array = array( + "//div[@id='sidebar-first']//fieldset[starts-with(@id, 'edit-render-example-block-fieldset')]", + '//*[@id="content"]//fieldset[contains(@id,"edit-render-example-block-fieldset")]', + ); + $this->assertRenderResults($xpath_array); + + + // Turn off block render array display and turn on the page render array + // display. + $edit = array( + 'render_example_show_page' => TRUE, + 'render_example_show_block' => FALSE, + ); + $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration')); + + $xpath_array = array( + '//*[@id="content"]//fieldset[starts-with(@id,"edit-render-example-page-fieldset")]', + ); + $this->assertRenderResults($xpath_array); + + // Add note about render arrays to the top of sidebar_first. + $edit = array( + 'render_example_note_about_render_arrays' => TRUE, + ); + $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration')); + $xpath_array = array( + '//*[@id="sidebar-first"]//ol//li[starts-with(.,"Render arrays are everywhere")]', + ); + $this->assertRenderResults($xpath_array); + + // Move the navigation menu to the top of the content area. + $edit = array( + 'render_example_move_navigation_menu' => TRUE, + ); + $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration')); + $xpath_array = array( + '//*[@id="content"]//h2[starts-with(.,"Navigation")]', + ); + $this->assertRenderResults($xpath_array); + + // Skip a test for reversing order of sidebar_first as I think it would + // be too fragile. + + // Test the addition of #prefix and #suffix + $edit = array( + 'render_example_prefix' => TRUE, + ); + $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration')); + $xpath_array = array( + '//*[@id="sidebar-first"]//*[contains(@class, "block-prefix")]/span[contains(@class, "block-suffix")]', + ); + $this->assertRenderResults($xpath_array); + + // Test some rendering facets of the various render examples + $this->drupalGet('examples/render_example/arrays'); + $content = $this->xpath('//*[@class="render-array"][1]'); + + $xpath_array = array( + '//div[@class="rendered"][starts-with(.,"Some basic text in a #markup")]' => 'Some basic text in a #markup (shows basic markup and how it is rendered)', + '//div[@class="rendered"][starts-with(.,"This is some text that should be put to")]' => 'This is some text that should be put together | This is some more text that we need | ', + '//div[@class="rendered"][starts-with(.,"The current time was")]' => 'The current time was when this was cached. Updated every seconds', + '//div[@class="rendered"]/div[text()][starts-with(.,"(prefix)This one")]' => '(prefix)This one adds a prefix and suffix, which put a div around the item(suffix)', + '//div[@class="rendered"]/div[text()][starts-with(.,"markup for pre_")]' => 'markup for pre_render and post_render example', + '//div[@class="rendered"]/div[text()][starts-with(.,"This markup was added")]' => 'This markup was added after rendering by a #post_render', + '//div[@class="rendered"]/div[text()][starts-with(.,"This #suffix")]' => 'This #suffix was added by a #pre_render', + ); + $this->assertRenderedText($xpath_array); + } + +} diff --git a/render_example/templates/render-array-help.html.twig b/render_example/templates/render-array-help.html.twig new file mode 100644 index 0000000..58b406a --- /dev/null +++ b/render_example/templates/render-array-help.html.twig @@ -0,0 +1,18 @@ +{# +/** + * @file + * Default theme implementation to render_array. + * + * Themes the render array (from the demonstration page). + * + * Available variables: + * - element: Element that will be rendered. + * - element['raw'] : Element printed as HTML + * - element['kpr'] : Krumo version of the rendered array + * + * @ingroup themeable + */ +#} +

Unrendered Element

+{{ element['raw'] }} +{{ element['kpr'] }} diff --git a/render_example/templates/render-array.html.twig b/render_example/templates/render-array.html.twig new file mode 100644 index 0000000..0a3d075 --- /dev/null +++ b/render_example/templates/render-array.html.twig @@ -0,0 +1,24 @@ +{# +/** + * @file + * Default theme implementation to render_array_help. + * + * Themes the render array help portion (from the demonstration page). + * + * Available variables: + * - element: Element that will be rendered. + * - element['rendered'] : rendered element + * - element['unrendered'] : unrendered element + * - element['description'] : the description of this example + * + * @ingroup themeable + */ +#} +
+

{{ element['description'] }}

+

Rendered Element

+ {{ element['rendered'] }} +

Unrendered Element

+ {{ element['unrendered'] }} + {{ element['kpr'] }} +
diff --git a/render_example/templates/render-example-add-div.html.twig b/render_example/templates/render-example-add-div.html.twig new file mode 100644 index 0000000..4253888 --- /dev/null +++ b/render_example/templates/render-example-add-div.html.twig @@ -0,0 +1,16 @@ +{# +/** + * @file + * Default theme implementation to render_example_add_div. + * + * Wraps a div around the already-rendered #children. + * + * Available variables: + * - element: Element that will be rendered. + * + * @ingroup themeable + */ +#} +
+ {{ element['#children'] }} +
diff --git a/render_example/templates/render-example-add-notes.html.twig b/render_example/templates/render-example-add-notes.html.twig new file mode 100644 index 0000000..ef90f45 --- /dev/null +++ b/render_example/templates/render-example-add-notes.html.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Default theme implementation to render_example_add_notes. + * + * Wraps a div and add a little text after the rendered #children. + * + * Available variables: + * - element: Element that will be rendered. + * + * @ingroup themeable + */ +#} +
+ {{ element['#children'] }} + {% trans %}This is a note added by a #theme_wrapper{% endtrans %} +
diff --git a/render_example/templates/render-example-aggregate.html.twig b/render_example/templates/render-example-aggregate.html.twig new file mode 100644 index 0000000..c7fd0ca --- /dev/null +++ b/render_example/templates/render-example-aggregate.html.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Default theme implementation to render_example_aggregate. + * + * The contents are rendered above feed listings when browsing source feeds. + * For example, "example.com/aggregator/sources/1". + * + * Available variables: + * - element: Element that will be rendered. + * + * @ingroup themeable + */ +#} +{%- for item in element.child -%} + {{ item }} {{ element['#separator'] }} +{%- endfor -%}