diff --git a/composer.json b/composer.json index 26d4063..f0b4f77 100644 --- a/composer.json +++ b/composer.json @@ -8,5 +8,8 @@ "documentation": "https://api.drupal.org/api/examples", "source": "http://cgit.drupalcode.org/examples" }, + "suggest": { + "drupal/devel": "Some modules will be able to pretty-print PHP with this module." + }, "license": "GPL-2.0+" } diff --git a/examples.module b/examples.module index 1fbcd6b..4441ce6 100644 --- a/examples.module +++ b/examples.module @@ -57,6 +57,7 @@ function examples_toolbar() { 'pager_example' => 'pager_example.page', 'phpunit_example' => 'phpunit_example_description', 'plugin_type_example' => 'plugin_type_example.description', + 'render_example' => 'render_example.description', 'simpletest_example' => 'simpletest_example_description', 'tabledrag_example' => 'tabledrag_example.description', 'stream_wrapper_example' => 'stream_wrapper_example.description', diff --git a/render_example/config/install/render_example.settings.yml b/render_example/config/install/render_example.settings.yml new file mode 100644 index 0000000..f1df332 --- /dev/null +++ b/render_example/config/install/render_example.settings.yml @@ -0,0 +1,6 @@ +show_block: false +show_page: false +note_about_render_arrays: false +move_breadcrumbs: false +reverse_sidebar: false +wrap_blocks: false diff --git a/render_example/config/schema/render_example.schema.yml b/render_example/config/schema/render_example.schema.yml new file mode 100644 index 0000000..ca8b19c --- /dev/null +++ b/render_example/config/schema/render_example.schema.yml @@ -0,0 +1,16 @@ +render_example.settings: + type: config_object + label: 'Render Example Settings' + mapping: + show_block: + type: boolean + show_page: + type: boolean + note_about_render_arrays: + type: boolean + move_breadcrumbs: + type: boolean + reverse_sidebar: + type: boolean + wrap_blocks: + type: boolean diff --git a/render_example/css/render_example.css b/render_example/css/render_example.css new file mode 100644 index 0000000..3f79120 --- /dev/null +++ b/render_example/css/render_example.css @@ -0,0 +1,15 @@ +.render_example--array { + border-bottom: 2px solid #000; + margin-top: 10px; + padding-left: 5px; + padding-top: 5px; +} + +.render_example--code { + background: #f1f1f1; + border: 1px solid #000; + display: block; + max-height: 300px; + overflow: scroll; + padding: 10px; +} diff --git a/render_example/render_example.info.yml b/render_example/render_example.info.yml new file mode 100644 index 0000000..d342177 --- /dev/null +++ b/render_example/render_example.info.yml @@ -0,0 +1,10 @@ +name: Render example +type: module +description: Provides examples demonstrating Drupal's Render API. +package: Example modules +core: 8.x +dependencies: + - drupal:block + - drupal:node + - drupal:user + - examples:examples diff --git a/render_example/render_example.libraries.yml b/render_example/render_example.libraries.yml new file mode 100644 index 0000000..785ea43 --- /dev/null +++ b/render_example/render_example.libraries.yml @@ -0,0 +1,9 @@ +# Define a new asset library which includes the CSS used to style the page +# at examples/render_example/arrays. See how asset libraries are attached to +# render arrays by reading the example code in +# Drupal\render_example\Controller\RenderExampleController::arrays(). +render-example.library: + version: 1.x + css: + theme: + css/render_example.css: {} diff --git a/render_example/render_example.links.menu.yml b/render_example/render_example.links.menu.yml new file mode 100644 index 0000000..e1df27d --- /dev/null +++ b/render_example/render_example.links.menu.yml @@ -0,0 +1,19 @@ +render_example.description: + title: Render Example + description: Examples of building and altering render arrays. + route_name: render_example.description + expanded: TRUE + +render_example.arrays: + title: Building Render Arrays + description: Building render arrays in controllers. + route_name: render_example.arrays + parent: render_example.description + weight: -9 + +render_example.altering: + title: Altering Render Arrays + description: Using hooks and callbacks to alter render arrays. + route_name: render_example.altering + parent: render_example.description + weight: -8 diff --git a/render_example/render_example.module b/render_example/render_example.module new file mode 100644 index 0000000..79524b4 --- /dev/null +++ b/render_example/render_example.module @@ -0,0 +1,240 @@ + ['render element' => 'element'], + 'render_array' => ['render element' => 'element'], + // This is used in combination with \Drupal\render_example\Element\Marquee + // to define a new custom render element type that allows for the use of + // '#type' => 'marquee' elements in a render array. + 'render_example_marquee' => [ + 'variables' => [ + 'content' => '', + 'attributes' => [], + ], + ], + ]; +} + +/** + * Example '#post_render' callback function. + * + * Post render callbacks are triggered after an element has been rendered to + * HTML and can act upon the final rendered string. + * + * This function is made use of by one of the examples in + * Drupal\render_example\Controller\RenderExampleController::arrays(). + * + * @param string $markup + * The rendered element. + * @param array $element + * The element which was rendered (for reference) + * + * @return string + * Markup altered as necessary. In this case we add a little postscript to it. + * + * @see \Drupal\render_example\Controller\RenderExampleController::arrays() + */ +function render_example_add_prefix($markup, array $element) { + $markup = $markup . '
This markup was added after rendering by a #post_render callback.
'; + return $markup; +} + +/** + * Example '#pre_render' function. + * + * Pre render callbacks are triggered prior to rendering an element to HTML and + * are given the chance to manipulate the renderable array. Any changes they + * make will be reflected in the final rendered HTML. + * + * This function is made use of by one of the examples in + * Drupal\render_example\Controller\RenderExampleController::arrays(). + * + * @param array $element + * The element which will be rendered. + * + * @return array + * The altered element. In this case we add a #prefix to it. + * + * @see \Drupal\render_example\Controller\RenderExampleController::arrays() + */ +function render_example_add_suffix(array $element) { + $element['#suffix'] = '
' . t('This #suffix was added by a #pre_render callback.') . '
'; + return $element; +} + +/** + * Implements hook_preprocess_page(). + * + * Demonstrates using a preprocess function to alter the renderable array that + * represents the page currently being viewed. + */ +function render_example_preprocess_page(&$variables) { + // Only modify the 'altering' page. + if (\Drupal::routeMatch()->getRouteName() != 'render_example.altering') { + return; + } + + $config = \Drupal::config('render_example.settings'); + + // Preprocess hooks are invoked by the theme layer, and are used to give + // modules a chance to manipulate the variables that are going to be made + // available to a specific template file. Since content is still defined as + // renderable arrays at this point you can do quite a bit to manipulate the + // eventual output by altering these arrays. + // + // The $page variable in this case contains the complete content of the page + // including all regions, and the blocks placed within each region. + // + // The actual process of converting a renderable array to HTML is started when + // this variable is printed out within a Twig template. Drupal's Twig + // extension provides a wrapper around the Twig code that prints out variables + // which checks to see if the variable being printed is a renderable array and + // passes it through \Drupal\Core\Render\RendererInterface::render() before + // printing it to the screen. + $page = &$variables['page']; + + // Move the breadcrumbs into the content area. + if ($config->get('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 contents of the sidebar in reverse order. + if ($config->get('reverse_sidebar') && !empty($page['sidebar_first'])) { + $page['sidebar_first'] = array_reverse($page['sidebar_first']); + foreach (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 current page. + // This relies on the Devel module's variable dumper service. + // https://wwww.drupal.org/project/devel + if (Drupal::moduleHandler()->moduleExists('devel') && $config->get('show_page')) { + $page['content']['page_render_array'] = [ + '#type' => 'markup', + '#prefix' => '

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

', + // The devel.dumper service is provided by the devel module and makes for + // and easier to read var_dump(). Especially if the companion Kint module + // is enabled. + 'dump' => \Drupal::service('devel.dumper')->exportAsRenderable($page, '$page'), + '#weight' => -99999, + ]; + + $page['content']['#sorted'] = FALSE; + } +} + +/** + * Implements hook_preprocess_block(). + */ +function render_example_preprocess_block(&$variables) { + // Only modify the 'altering' page. + if (\Drupal::routeMatch()->getRouteName() != 'render_example.altering') { + return; + } + + $config = \Drupal::config('render_example.settings'); + + // This example shows how you can manipulate an existing renderable array. In + // this case by adding #prefix and #suffix properties to the block in order to + // wrap a
around it. + if ($config->get('wrap_blocks')) { + $variables['content']['#prefix'] = '

' . t('Prefixed') . '

'; + $variables['content']['#suffix'] = '' . t('Block suffix') . '
'; + } + + // Show the render array used to build each block if the Devel module is + // installed and the feature is enabled. + if (Drupal::moduleHandler()->moduleExists('devel') && $config->get('show_block')) { + $variables['content']['block_render_array'] = [ + '#type' => 'markup', + '#prefix' => '

' . t('The block render array for @block_id.', ['@block_id' => $variables['plugin_id']]) . '

', + 'dump' => \Drupal::service('devel.dumper')->exportAsRenderable($variables, $variables['plugin_id']), + ]; + } +} + +/** + * @} 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..5f93efa --- /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: + _permission: 'access content' + +render_example.altering: + path: 'examples/render-example/altering' + defaults: + _form: '\Drupal\render_example\Form\RenderExampleDemoForm' + _title: 'Alter pages and blocks' + requirements: + _permission: 'access content' + +render_example.arrays: + path: 'examples/render-example/arrays' + defaults: + _controller: '\Drupal\render_example\Controller\RenderExampleController::arrays' + requirements: + _permission: 'access content' diff --git a/render_example/src/Controller/RenderExampleController.php b/render_example/src/Controller/RenderExampleController.php new file mode 100644 index 0000000..296dce5 --- /dev/null +++ b/render_example/src/Controller/RenderExampleController.php @@ -0,0 +1,498 @@ +currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + protected function getModuleName() { + return 'render_example'; + } + + /** + * Examples of defining content using renderable arrays. + * + * Methods on a controller that are the target of a route should return a + * renderable array which contains any content to display for that route. + */ + public function arrays() { + // The core structure of the Render API is the render array, which is a + // hierarchical associative array containing data to be rendered and + // properties describing how the data should be rendered. Whenever a module + // needs to output content it should do so be defining that content as a + // renderable array. Below we'll look at some common examples of how render + // arrays can be used to define content. + $build = []; + + // CSS and JavaScript libraries can be attached to elements in a renderable + // array. This way, if the element ends up being rendered and displayed you + // know for sure the CSS/JavaScript will also be included. But, if for + // some reason the element isn't ever rendered then Drupal can skip the + // unnecessary extra files. + // + // Learn more about attaching CSS and JavaScript libraries with the + // #attached property here: + // https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/#sec_attached + $build['#attached'] = [ + 'library' => [ + 'render_example/render-example.library', + ], + ]; + + // Renderable arrays have two kinds of key/value pairs: properties and + // children. Properties have keys starting with '#' and their values + // influence how the array will be translated to a string. Children are all + // elements whose keys do not start with a '#'. Their values should be + // renderable arrays themselves. + // + // This example defines a new element, 'simple', that contains two + // properties; '#markup' and '#description'. This is the quickest way to + // output a string of HTML. + $build['simple'] = [ + '#markup' => '

' . $this->t('This page contains examples of various content elements described using render arrays. Read the code and comments in \Drupal\render_example\Controller\RenderExampleController::arrays() for more information.') . '

', + '#description' => $this->t('Example of using #markup'), + ]; + + // Additional properties can be used to further define the content. In this + // case '#prefix' and '#suffix' are being used to provide strings to add + // before, and after, the main content. This is useful because now the tag + // being used to wrap the block of content can be easily changed without + // having to worry about the content. Or, the tag can easily be left out + // during rendering if for example the content is being output as JSON. + // + // There is a set of common properties that can be used for all elements in + // a render array. These are defined by + // \Drupal\Core\Render\Element\RenderElement. Most elements also have + // additional element type specific properties. + // + // Figuring out what additional properties are available requires first + // determining what sort of render element you're dealing with. Look for the + // presence of one of these properties to start: + // - #markup, or #plain_text: These are the simplest render arrays, and are + // used to display simple strings of text. In addition to the common set + // of properties available for all elements #markup elements can use the + // #allowed_tags property, an array of additional tags to allow when the + // HTML string is run through \Drupal\Component\Utility\Xss::filterAdmin() + // to strip out possible XSS vectors. + // - #theme: The presence of #theme indicates that the array contains data + // to be themed by a particular theme hook. The available properties will + // depend on the specific theme hook. See the example below for more about + // determining what properties to use. + // - #type: The presence of #type indicates that the array contains data and + // options for a particular type of "render element" (for example, 'form', + // 'textfield', 'submit', for HTML form element types; 'table', for a + // table with rows, columns, and headers). The additional properties will + // depend on the render element type, and are documented on the class that + // defines the element type. + // + $build['simple_extras'] = [ + '#description' => $this->t('Example of using #prefix and #suffix'), + // Note the addition of '#type' => 'markup' in this example compared to + // the one above. Because #markup is such a commonly used element type you + // can exclude the '#type' => 'markup' line and it will be assumed + // automatically if the '#markup' property is present. + '#type' => 'markup', + '#markup' => '

' . $this->t('This one adds a prefix and suffix, which put a blockqoute tag around the item.') . '

', + '#prefix' => '
', + '#suffix' => '
', + ]; + + // In addition to #markup, you can also use #plain_text to output, you + // guessed it, strings of plain text. This indicates that the array contains + // text which should be escaped before it is displayed. + $build['simple_text'] = [ + '#plain_text' => 'This is escaped', + '#description' => $this->t('Example of using #plain_text'), + ]; + + // Using the '#theme' property for an element specifies that the array + // contains data to be themed by a particular theme hook. Essentially using + // a Twig template to generate the HTML for an element. Modules define theme + // hooks by implementing hook_theme(), which specifies the input "variables" + // used to provide data and options; if a hook_theme() implementation + // specifies variable 'separator', then in a render array, you would provide + // this data using the '#separator' property. + // + // @see hook_theme() + $build['theme_element'] = [ + // The '#theme' property can be set to any valid theme hook. For more + // information about theme hooks, and to discover available theme hooks + // that you can use when creating render arrays see the documentation for + // hook_theme(). + // + // Many of the most commonly used theme hooks are defined in + // drupal_common_theme(). + '#theme' => 'item_list', + '#title' => $this->t('Example of using #theme'), + // The #items property is specific to the 'item_list' theme hook, and + // corresponds to the variable {{ items }} in the item-list.twig.html + // template file. + '#items' => [ + $this->t('This is an item in the list'), + $this->t('This is some more text that we need in the list'), + ], + ]; + + // Using the '#type' property for an element specifies that the array + // contains data and options for a particular type of "render element". + // Render element types can be thought of as prepackaged render arrays that + // provide default values for a set of properties as well as code that + // will perform additional processing on the array before it is rendered. + // + // As an example take a look at the code in + // \Drupal\Core\Render\Element\Table::getInfo(). Notice that it is defining + // values for #theme, and #process? These values will be merged with + // whatever properties you define in your code. + // + // In addition, most render element types have type specific properties. A + // table for example has #header, and #rows properties. The easiest way to + // determine what element type specific properties exist is to read the + // documentation for the class that defines the element type. Don't forget + // that it will also inherit properties used by any class it is extending. + // + // There are two types of render element types: + // - Generic elements: Generic render element types encapsulate logic for + // generating HTML and attaching relevant CSS and JavaScript to the page. + // These include things like link, table, and drop button elements. + // - Form elements: Most of the render element types provided by core + // represent the various widgets you might use on a form. Text fields, + // password fields, and file upload buttons for example. These elements + // are intended to be used in conjunction with a form controller class + // and have additional properties such as `#required`, and + // `#element_validate`, related to their use as part of a form. For more + // on form elements check out the fapi_example module. + $build['table'] = [ + // The value used for #type is the ID of the plugin that implements the + // element type you want to use. This can be inferred from the annotation + // for the element. + // You can also find a list of element types provided by Drupal core here + // https://api.drupal.org/api/drupal/elements. + '#type' => 'table', + '#caption' => $this->t('Our favorite colors.'), + '#header' => [$this->t('Name'), $this->t('Favorite color')], + '#rows' => [ + [$this->t('Amber'), $this->t('teal')], + [$this->t('Addi'), $this->t('green')], + [$this->t('Blake'), $this->t('#063')], + [$this->t('Enid'), $this->t('indigo')], + [$this->t('Joe'), $this->t('green')], + ], + '#description' => $this->t('Example of using #type.'), + ]; + + // Render arrays can be nested any level deep. This allows you to group + // like things together. A great example of this is the $page array used in + // conjunction with the page.html.twig template. The top level contains all + // the regions, each of which contain the blocks placed in that region, + // which in turn contain their own content. In fact, when this array is + // ultimately displayed on a page it will be as part of the $page array. + $build['nested_example'] = [ + '#description' => $this->t('Example of nesting elements'), + '#markup' => '

' . $this->t('Render arrays can contain any number of nested elements. During rendering, the innermost elements are rendered first, and their output is incorporated into the parent element.') . '

', + 'nested_child_element' => [ + // An un-ordered list of links. + // See /core/modules/system/templates/item-list.html.twig. + '#theme' => 'item_list', + '#title' => $this->t('Links'), + '#list_type' => 'ol', + '#items' => [ + Link::fromTextAndUrl($this->t('Drupal'), Url::fromUri('https://www.drupal.org')), + Link::fromTextAndUrl($this->t('Not Drupal'), Url::fromUri('https://wordpress.org/')), + ], + ], + ]; + + // Example of adding a link using the #link element type. + $build['nested_example']['another_nested_child'] = [ + // See \Drupal\Core\Render\Element\Link. + '#type' => 'link', + '#title' => $this->t('A link to example.com'), + '#url' => Url::fromUri('https://example.com'), + ]; + + // The #theme_wrappers property can be used to provide an array of theme + // hooks which provide the envelope or "wrapper" of a set of child elements. + // The theme function finds its element children (the sub-arrays) already + // rendered in '#children'. + $build['theme_wrappers demonstration'] = [ + '#description' => $this->t('Example of using #theme_wrappers'), + 'child1' => ['#markup' => $this->t('Markup for child1')], + 'child2' => ['#markup' => $this->t('Markup for child2')], + '#theme_wrappers' => [ + 'render_example_add_div', + ], + ]; + + // Use the #access property to control who can see what content. If an + // element in an render array has its #access property set to FALSE it will + // be removed from the array before rendering. And thus not visible. + $build['access_example'] = [ + '#description' => $this->t('Example of using #access to control visibility'), + '#markup' => $this->t('This text is only visible to authenticated users.'), + '#access' => $this->currentUser->isAuthenticated(), + ]; + + // Some properties define callbacks, which are callable functions or methods + // that are triggered at specific points during the rendering pipeline. + $build['pre_render and post_render'] = [ + '#description' => $this->t('Example of using #pre_render and #post_render'), + '#markup' => '
' . t('markup for pre_render and post_render example') . '
', + // #pre_render callbacks are triggered early in the rendering process, + // they get access to the element in the array where the callback is + // named, and all of its children. They can be used to do things like + // conditionally alter the value of a property prior to the array being + // rendered to HTML. + '#pre_render' => ['render_example_add_suffix'], + // #post_render callbacks are triggered after the array has been rendered + // and can operate on the rendered HTML. They also have access to the + // original array for context. + '#post_render' => ['render_example_add_prefix'], + ]; + + // Properties that contain callbacks can also reference methods on a class + // in addition to functions. See + // \Drupal\render_example\Controller\RenderExampleController::preRender() + // @todo: This doesn't work, we need to fix it. + // $build['#pre_render'] = [static::class, 'preRender']; + + // Caching is an important part of the Render API, converting an array to a + // string of HTML can be an expensive process, and therefore whenever + // possible the Render API will cache the results of rendering an array in + // order to improve performance. + // + // When defining a render array you should use the #cache property to define + // the cachability of an element. + $build['cache_demonstration'] = [ + '#description' => $this->t('#cache demonstration'), + // This string contains information that is specific to the user who is + // currently viewing the page. We can cache it, and re-use the string any + // time the same user views the page again. However, if the user changes, + // or if the user changes their name, we need to expire the cached data + // and rebuild it so that it is accurate. + '#markup' => $this->t('Hello @name, welcome to the #cache example.', ['@name' => $this->currentUser->getAccountName()]), + // The #cache property is used to provide metadata about the element being + // cached, and the conditions under which it should be expired. This can + // be time based, or context based. You can read more about caching + // render arrays here + // https://www.drupal.org/docs/8/api/render-api/cacheability-of-render-arrays + '#cache' => [ + // The "current user" is used above, which depends on the request, so + // we tell Drupal to vary by the 'user' cache context. + 'contexts' => [ + 'user', + ], + ], + ]; + + // A #lazy_builder callback can be used to build a highly dynamic section of + // a render array from scratch. This, combined with the use of placeholders, + // allows the renderer to cache some, but not all, portions of a render + // array. Without #lazy_builders, if any element in the render tree is + // uncacheable the whole tree would need to be re-rendered every time. + // + // The general rendering flow is as follows: + // - Check for cached version of output from previous rendering, if it + // exists replace any placeholders in the rendered output with their + // dynamic content as generated by the #lazy_builder callback, and return + // the resulting HTML. + // - If no cached version exists render the array to HTML, when an element + // that can be placeholdered is encountered insert a placeholder, cache + // the HTML after rendering for next time, replace the placeholders with + // their dynamic content, and return the resulting HTML. + // + // This is especially noticeable when used in conjunction with modules like + // Big Pipe which do rendering of a page in multiple passes vs. the default + // single flush renderer. + // + // See \Drupal\block\BlockViewBuilder::viewMultiple() for an example from + // core. + $build['lazy_builder'] = [ + // Set the value of the #lazy_builder property to an array, the first key + // of the array is the method, service, or function, to call in oder to + // generate the dynamic data. The second argument is an array of any + // arguments to pass to the callback. Arguments can be only primitive + // types (string, bool, int, float, NULL). + '#lazy_builder' => [ + static::class . '::lazyBuilder', + [$this->currentUser->id(), 'Y-m-d'], + ], + // #lazy_builder callbacks can be used in conjunction with + // #create_placeholder to tell the renderer that instead of simply calling + // the #lazy_builder code right away, to instead insert a placeholder and + // delay execution of the #lazy_builder code until it's needed. + // + // This is somewhat analogous to the way Drupal uses the PSR-4 autoloading + // standard to "lazy" load PHP files that contain the definition of a + // class only if, and when, that class is used. + // + // To force a element to use a placeholder set #create_placeholder to + // TRUE. + // + // Alternatively you could include #cache metadata (see above) and allow + // the Render API to use that metadata to automatically determine based on + // the existence of high-cardinality cache contexts in the subtree whether + // or not the element should use a placeholder. + '#create_placeholder' => TRUE, + ]; + + // Example of the marquee element type defined by + // \Drupal\render_example\Element\Marquee. + $build['marquee'] = [ + '#description' => $this->t('Example custom element type'), + '#type' => 'markup', + 'marquee_element' => [ + '#type' => 'marquee', + '#content' => $this->t('Hello world!'), + ], + ]; + + $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 (Element::children($build) as $key) { + if (isset($build[$key])) { + $output[$key] = [ + '#theme' => 'render_array', + 'description' => [ + '#type' => 'markup', + '#markup' => isset($build[$key]['#description']) ? $build[$key]['#description'] : '', + ], + 'rendered' => $build[$key], + 'unrendered' => [ + '#type' => 'markup', + '#markup' => htmlentities(\Drupal\Component\Utility\Variable::export($build[$key])), + ], + ]; + } + } + + foreach (Element::properties($build) as $key) { + $output[$key] = $build[$key]; + } + + return $output; + } + + /** + * A #pre_render callback, expand array to include additional example info. + * + * This method is called during the process of rendering the array generated + * by \Drupal\render_example\Controller\RenderExampleController::arrays(). + * + * This also demonstrates how a #pre_render callback could be used to expand + * a relatively simple array into multiple individual renderable elements + * based on application logic. + * + * @param array $element + * Pre render methods (and functions) get a single argument that is the + * render API array representing the element where the #pre_render property + * was defined, and all of it's children. + * + * @return array + * Pre render methods (and functions) should return the modified render + * array. + */ + public static function preRender(array $element) { + // For each first level child element lets add some additional helpful + // output. \Drupal\Core\Render\Element::children() is a utility method that + // allows you to quickly identify all children of a render array. That is + // those key/value pairs whose key does not start with a '#'. + foreach (Element::children($element) as $key) { + $child = $element[$key]; + unset($element[$key]); + if (isset($child['#description'])) { + $element[$key] = [ + // The value from the #description property will be used as a title + // for this element in the final output. + 'description' => [ + '#markup' => $child['#description'], + ], + // Move the original element to 'rendered'. The rendering process is + // recursive so this will still be located, and rendered to HTML. + 'rendered' => $child, + // Export the element definition as a string of text so we can display + // the array that was used to create the rendered output just below the + // output. + 'unrendered' => [ + '#markup' => htmlentities(Variable::export($child)), + ], + '#theme' => 'render_array', + ]; + } + } + + // Return our modified version of the original $element. + return $element; + } + + /** + * Example #lazy_builder callback. + * + * Demonstrates the use of a #lazy_builder callback to build out a render + * array that can be substituted into the parent array wherever the cacheable + * placeholder exists. + * + * This method is called during the process of rendering the array generated + * by \Drupal\render_example\Controller\RenderExampleController::arrays(). + * + * @param string $date_format + * Date format to use with \Drupal\Core\Datetime\DateFormatter::format(). + * + * @return array + * A renderable array with content to replace the #lazy_builder placeholder. + */ + public static function lazyBuilder($date_format) { + $build = [ + 'lazy_builder_time' => [ + '#markup' => '

' . \Drupal::translation()->translate('The current time is @time', [ + '@time' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'long'), + ]) . '

', + ], + ]; + + return $build; + } + +} diff --git a/render_example/src/Element/Marquee.php b/render_example/src/Element/Marquee.php new file mode 100644 index 0000000..1a5c301 --- /dev/null +++ b/render_example/src/Element/Marquee.php @@ -0,0 +1,92 @@ + 'marquee', + * '#content' => 'Whoa cools, a marquee!', + * ]; + * @endcode + * + * View an example of this custom element in use in + * \Drupal\render_example\Controller\RenderExampleController::arrays(). + * + * @see plugin_api + * @see render_example_theme() + * + * @RenderElement("marquee") + */ +class Marquee extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + + // Returns an array of default properties that will be merged with any + // properties defined in a render array when using this element type. + // You can use any standard render array property here, and you can also + // custom properties that are specific to your new element type. + return [ + // See render_example_theme() where this new theme hook is declared. + '#theme' => 'render_example_marquee', + // Define a default #pre_render method. We will use this to handle + // additional processing for the custom attributes we add below. + '#pre_render' => [ + [self::class, 'preRenderMarquee'], + ], + // This is a custom property for our element type. We set it to blank by + // default. The expectation is that a user will add the content that they + // would like to see inside the marquee tag. This custom property is + // accounted for in the associated template file. + '#content' => '', + '#attributes' => [ + 'direction' => 'left', + 'loop' => -1, + 'scrollamount' => 'random', + ], + ]; + } + + /** + * Pre-render callback; Process custom attribute options. + * + * @param array $element + * The renderable array representing the element with '#type' => 'marquee' + * property set. + * + * @return array + * The passed in element with changes made to attributes depending on + * context. + */ + public static function preRenderMarquee(array $element) { + // Normal attributes for a tag do not include a 'random' option + // for scroll amount. Our marquee element type does though. So we use this + // #pre_render callback to check if the element was defined with the value + // 'random' for the scrollamount attribute, and if so replace the string + // with a random number. + if ($element['#attributes']['scrollamount'] == 'random') { + $element['#attributes']['scrollamount'] = abs(rand(1, 50)); + } + return $element; + } + +} diff --git a/render_example/src/Form/RenderExampleDemoForm.php b/render_example/src/Form/RenderExampleDemoForm.php new file mode 100644 index 0000000..539c0be --- /dev/null +++ b/render_example/src/Form/RenderExampleDemoForm.php @@ -0,0 +1,286 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('render_example.settings'); + + $form['description'] = [ + '#markup' => $this->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 examples of altering the page late in its lifecycle.'), + ]; + + $form['show_arrays'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Show render arrays'), + 'render_example_show_block' => [ + '#type' => 'checkbox', + '#title' => $this->t('Show block render arrays'), + '#default_value' => $config->get('show_block'), + // Only enable this option if the Devel module is enabled. + '#access' => $this->moduleHandler->moduleExists('devel'), + ], + 'render_example_show_page' => [ + '#type' => 'checkbox', + '#title' => $this->t('Show page render arrays'), + '#default_value' => $config->get('show_page'), + // Only enable this option if the Devel module is enabled. + '#access' => $this->moduleHandler->moduleExists('devel'), + ], + 'render_example_devel' => [ + '#markup' => $this->t('Install the Devel module (https://www.drupal.org/project/devel) to enable additional demonstration features.'), + // Only display this if the Devel module is not already installed. + '#access' => !$this->moduleHandler->moduleExists('devel'), + ], + ]; + + $form['page_fiddling'] = [ + '#type' => 'fieldset', + '#title' => t('Make changes on all pages via hook_preprocess_page()'), + '#description' => $this->t('Theses changes are all made via the function render_example_preprocess_page()'), + 'render_example_move_breadcrumbs' => [ + '#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('move_breadcrumbs'), + ], + 'render_example_reverse_sidebar' => [ + '#title' => t('Reverse ordering of sidebar_first elements (if it exists)'), + '#description' => t('Uses hook_preprocess_page() to reverse the ordering of items in sidebar_first'), + '#type' => 'checkbox', + '#default_value' => $config->get('reverse_sidebar'), + ], + 'render_example_wrap_blocks' => [ + '#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('wrap_blocks'), + ], + ]; + + $form['tabledrag'] = [ + '#type' => 'table', + '#id' => 'draggable-table', + '#caption' => $this->t('Our favorite colors.'), + '#header' => [ + $this->t('Name'), + $this->t('Favorite color'), + $this->t('Weight'), + ], + // #tabledrag and be used on #table elements in the context of a form. + // When enabled, the table will be rendered with a drag & drop interface + // that can be used to re-order elements within the table. Any changes you + // make to the order will be made available to your validation and submit + // handlers via values in $form_state->getValues(). + // + // The #tabledrag property contains an array of options passed to the + // drupal_attach_tabledrag() function. These options are used to generate + // the necessary JavaScript settings to configure the tableDrag behavior + // for this table. + // + // For more in-depth documentation of the options below see + // drupal_attach_tabledrag(). + '#tabledrag' => [ + [ + // The HTML ID of the table to make draggable. See #id above. + 'table_id' => 'draggable-table', + // The action to be done on the form item. Either 'match' 'depth', or + // 'order'. + 'action' => 'order', + // String describing where the "action" option should be performed. + // Either 'parent', 'sibling', 'group', or 'self'. + 'relationship' => 'sibling', + // Class name applied on all related form elements for this action. + // See below. + 'group' => 'table-order-weight', + ], + ], + // Rather than defining the values to insert into the table using the + // #rows property you can define each row as a sub element of the table + // render array. And each column in the row as a sub element of the row + // array. + [ + // Apply the 'draggable' class to each row in the table that you want to + // be made draggable. + '#attributes' => ['class' => ['draggable']], + // The first two columsn are render arrays that display a string of + // text. + 'name' => ['#plain_text' => $this->t('Amber')], + 'color' => ['#plain_text' => $this->t('teal')], + // The third column is a #weight form field. + 'weight' => [ + '#type' => 'weight', + '#title_display' => 'invisible', + // Set the default value to whatever the current weight, or order, of + // the element that this row represents is. + '#default_value' => 1, + // Set a class on each field that represents the value to manipulate + // when the table is reordered. The name of this class should match + // the value used for the 'group' argument in the #tabledrag property + // above. + '#attributes' => ['class' => ['table-order-weight']], + ], + ], + // The rest of this array is additional rows so there is some data in the + // table to drag around. + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Addi')], + 'color' => ['#plain_text' => $this->t('green')], + 'weight' => [ + '#type' => 'weight', + '#title_display' => 'invisible', + '#default_value' => 2, + '#attributes' => ['class' => ['table-order-weight']], + ], + ], + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Blake')], + 'color' => ['#plain_text' => $this->t('#063')], + 'weight' => [ + '#type' => 'weight', + '#title_display' => 'invisible', + '#default_value' => 3, + '#attributes' => ['class' => ['table-order-weight']], + ], + ], + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Enid')], + 'color' => ['#plain_text' => $this->t('indigo')], + 'weight' => [ + '#type' => 'weight', + '#title_display' => 'invisible', + '#default_value' => 4, + '#attributes' => ['class' => ['table-order-weight']], + ], + ], + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Joe')], + 'color' => ['#plain_text' => $this->t('green')], + 'weight' => [ + '#type' => 'weight', + '#title_display' => 'invisible', + '#default_value' => 5, + '#attributes' => ['class' => ['table-order-weight']], + ], + ], + ]; + + $form['tableselect'] = [ + '#type' => 'table', + '#caption' => $this->t('Our favorite colors.'), + '#header' => [ + $this->t('Name'), + $this->t('Favorite color'), + ], + '#tableselect' => TRUE, + // Rather than defining the values to insert into the table using the + // #rows property you can define each row as a sub element of the table + // render array. And each column in the row as a sub element of the row + // array. + [ + 'name' => ['#plain_text' => $this->t('Amber')], + 'color' => ['#plain_text' => $this->t('teal')], + ], + // The rest of this array is additional rows so there is some data in the + // table. + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Addi')], + 'color' => ['#plain_text' => $this->t('green')], + ], + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Blake')], + 'color' => ['#plain_text' => $this->t('#063')], + ], + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Enid')], + 'color' => ['#plain_text' => $this->t('indigo')], + ], + [ + '#attributes' => ['class' => ['draggable']], + 'name' => ['#plain_text' => $this->t('Joe')], + 'color' => ['#plain_text' => $this->t('green')], + ], + ]; + + 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('show_block', $values['render_example_show_block'])->save(); + $config->set('show_page', $values['render_example_show_page'])->save(); + $config->set('move_breadcrumbs', $values['render_example_move_breadcrumbs'])->save(); + $config->set('reverse_sidebar', $values['render_example_reverse_sidebar'])->save(); + $config->set('wrap_blocks', $values['render_example_wrap_blocks'])->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/render_example/templates/description.html.twig b/render_example/templates/description.html.twig new file mode 100644 index 0000000..8332183 --- /dev/null +++ b/render_example/templates/description.html.twig @@ -0,0 +1,23 @@ +{# + +Description text for the Render API Example. + +#} + +{% set arrays_url = path('render_example.arrays') %} +{% set alter_url = path('render_example.altering') %} + +{% trans %} +

What are render arrays?

+ +

+ These are examples of how to construct render arrays.

+ +

+ You use render arrays to specify to Drupal how to construct HTML content.

+ + +{% endtrans %} diff --git a/render_example/templates/render-array.html.twig b/render_example/templates/render-array.html.twig new file mode 100644 index 0000000..09008c2 --- /dev/null +++ b/render_example/templates/render-array.html.twig @@ -0,0 +1,25 @@ +{# +/** + * @file + * Default theme implementation for 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'] }}
+
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..c1d107d --- /dev/null +++ b/render_example/templates/render-example-add-div.html.twig @@ -0,0 +1,16 @@ +{# +/** + * @file + * Default theme implementation for 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-marquee.html.twig b/render_example/templates/render-example-marquee.html.twig new file mode 100644 index 0000000..11f046e --- /dev/null +++ b/render_example/templates/render-example-marquee.html.twig @@ -0,0 +1,14 @@ +{# +/** + * @file + * Default theme implementation for the marquee render element. + * + * Available variables: + * - attributes: Attributes for the marguee tag. + * - content: The text to display within the marquee tag. + * + * @ingroup themeable + * @ingroup render_example + */ +#} +{{ content }}
diff --git a/render_example/tests/src/Functional/RenderExampleMenuTest.php b/render_example/tests/src/Functional/RenderExampleMenuTest.php new file mode 100644 index 0000000..990501a --- /dev/null +++ b/render_example/tests/src/Functional/RenderExampleMenuTest.php @@ -0,0 +1,72 @@ +drupalLogin( + $this->createUser(['access content', 'access user profiles']) + ); + + $assertion = $this->assertSession(); + + // Routes with menu links, and their form buttons. + $routes = [ + 'render_example.description' => [], + 'render_example.altering' => ['Save configuration'], + 'render_example.arrays' => [], + ]; + + // Ensure the links appear in the tools menu sidebar. + $this->drupalGet(''); + foreach (array_keys($routes) as $route) { + $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); + } + + // Go to all the routes and click all the buttons. + $routes = array_merge($routes, $routes); + foreach ($routes as $route => $buttons) { + $path = Url::fromRoute($route); + $this->drupalGet($path); + $assertion->statusCodeEquals(200); + foreach ($buttons as $button) { + $this->drupalPostForm($path, [], $button); + $assertion->statusCodeEquals(200); + } + } + } + +} diff --git a/render_example/tests/src/Functional/RenderExampleTest.php b/render_example/tests/src/Functional/RenderExampleTest.php new file mode 100644 index 0000000..382de4d --- /dev/null +++ b/render_example/tests/src/Functional/RenderExampleTest.php @@ -0,0 +1,120 @@ +createUser([ + 'access content', + ]); + $this->drupalLogin($web_user); + + $session = $this->assertSession(); + + $altering_url = Url::fromRoute('render_example.altering'); + + $this->drupalGet($altering_url); + // Make sure we're telling the user about devel. + $session->pageTextContains('Install the Devel module (https://www.drupal.org/project/devel) to enable additional demonstration features.'); + + // Test moving the breadcrumb to the top of the content region. Since we + // just installed render_example and the config defaults to FALSE for all + // the alter options, we shouldn't have to manage state before making + // assertions. + $breadcrumb_xpath = "//main[@id='content']//div[contains(@class, 'block-system-breadcrumb-block')]"; + $this->assertEmpty($this->xpath($breadcrumb_xpath)); + // Move the breadcrumbs to content region. + $this->drupalPostForm( + $altering_url, + [ + 'render_example_move_breadcrumbs' => TRUE, + 'render_example_reverse_sidebar' => FALSE, + 'render_example_wrap_blocks' => FALSE, + ], + t('Save configuration') + ); + $this->assertNotEmpty($this->xpath($breadcrumb_xpath)); + + // Test reversing order of items in region sidebar-first. Get the node + // elements under the sidebar region div. + $breadcrumb_xpath = "//div[contains(@class,'region-sidebar-first')]/*"; + $elements = $this->xpath($breadcrumb_xpath); + // There should be two elements, a div and then a nav. + $this->assertEquals('div', $elements[0]->getTagName()); + $this->assertTrue($elements[0]->hasClass('block-search')); + $this->assertEquals('nav', $elements[1]->getTagName()); + $this->drupalPostForm( + $altering_url, + [ + 'render_example_move_breadcrumbs' => FALSE, + 'render_example_reverse_sidebar' => TRUE, + 'render_example_wrap_blocks' => FALSE, + ], + t('Save configuration') + ); + // Get the elements again. + $elements = $this->xpath($breadcrumb_xpath); + // There should be two elements, a nav and then a div. + $this->assertEquals('nav', $elements[0]->getTagName()); + $this->assertEquals('div', $elements[1]->getTagName()); + $this->assertTrue($elements[1]->hasClass('block-search')); + + // Test wrapping blocks in divs. + $xpath = "//div[contains(@class,'block')]//div[@class='content']/div[@class='block-prefix']"; + $this->assertEmpty($this->xpath($xpath)); + $this->drupalPostForm( + $altering_url, + [ + 'render_example_move_breadcrumbs' => FALSE, + 'render_example_reverse_sidebar' => FALSE, + 'render_example_wrap_blocks' => TRUE, + ], + t('Save configuration') + ); + $this->assertNotEmpty($this->xpath($xpath)); + + // Test some rendering facets of the various render examples. + $this->drupalGet(Url::fromRoute('render_example.arrays')); + + $xpath_array = [ + // @todo: Add more of these. + 'foof' => 'Hello ' . $web_user->getAccountName() . ', welcome to the #cache example.', + ]; + foreach($xpath_array as $key => $value) { + $session->pageTextContains($value); + } + } + +}