Since it is important in D8 for module developers to create render arrays (controller functions build and return either a render array or a Symfony Response object), we should document clearly how to create a render array and what are the basic elements it should specify.
I've been looking at the following but still have some questions.

Render API mentions a few things:
Render arrays (at each level in the hierarchy) will usually have one of the following three properties defined:

  • #type type of element: 'page', 'form', 'table', etc.
  • #theme name of theme; by existing indicates that render array contains data to be themed by a particular theme hook
  • #markup HTML markup

Questions:
1) Is it required to have #type specified?
2) Is it required to have one of #theme or #markup in the render array? Are there other properties that would be alternatives to these two?
3) Do any of #type, #theme, #markup conflict with each other? It would appear that #theme and #markup do.
4) It doesn't sound useful for anything, but could a render array be empty? e.g. $render_array = array();
5) Is there a list of all possible values for #type specified somewhere?

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 rendered.
  • Children are all elements whose keys do not start with a '#'. Their values should be renderable arrays themselves.

6) Is this accurate? (Double-checking because, if true, there technically wouldn't be a reason to use '#'; perhaps it's for historical reasons?)

7) Are there other properties that are highly-recommended to be added to a basic render array?
for ex., it appears in Cacheability of render arrays that #cache is.

8) Does Drupal add default properties or children to a render array? Is it simple to document what these are, or does it vary a lot depending on the context? Are there any properties/children that are added to every render array?

Comments

Mark_L6n’s picture

Issue summary: View changes
tim.plunkett’s picture

Priority: Major » Normal
Status: Active » Fixed

1,2,3) You must have #type, #theme, or #markup, but only one. If you have #markup, it converts that to '#type' => 'markup' for you
4) That won't do anything
5) See #1617948: [policy for now] New standard for documenting form/render elements and properties
6) Correct
7) No, nothing is highly recommended
8) Default properties are added for each #type

Mark_L6n’s picture

Thanks for responding to everything! However, in the meantime I was experimenting and found a different result than:

1,2,3) You must have #type, #theme, or #markup, but only one. If you have #markup, it converts that to '#type' => 'markup' for you

It appears that #type and #theme can be used together, at least in the following case:

'#type' => 'page',
'#theme' => 'mytheme',

Results:
contains '#type' => 'page':
<body class="layout-no-sidebars ...">
no '#type' => 'page':
<body class="layout-one-sidebar layout-sidebar-first ...">
The theme twig file was used in both cases.
Although this is a different result from what is suggested, this is very useful in this case! I wanted some full screen images without sidebars, and this allowed it. So, at least in this case, it's beneficial that both #type and #theme are able to be specified.
So now the question arises, can #type and #theme be used together for all or specific other values of #type. Perhaps, if as suggested in #1617948: [policy for now] New standard for documenting form/render elements and properties that each #type will be documented, a part of that could be whether or not that #type of element works with #theme. (That is, if not all of them do.)

Fabianx’s picture

It works like this:

#type can define a default #theme, e.g. #type => pager, adds #theme => pager automatically.

This was done in Drupal 7 via hook_element_info() (where this is the same).

However this additions are done via:

$array += $defaults;

Hence any things you set in the render array will overwrite any default definition that your type sets.

I hope this helps :).

Mark_L6n’s picture

Thanks! Saying that '#type can define a default #theme' helped me figure out what was going on here.
'#type' => 'page' indeed by default defines '#theme' => 'page'.
This would cause the twig template core/modules/system/templates/page.html.twig to get loaded, unless a theme replaced it. I was using the Bartik theme, so core/themes/bartik/templates/page.html.twig would get loaded instead by default.
Then, when I placed in the render array:

'#type' => 'page',
'#theme' => 'mytheme',

that caused mytheme.html.twig to be used instead of core/themes/bartik/templates/page.html.twig.
However, that still doesn't fully explain the difference noted above:
contains '#type' => 'page':
<body class="layout-no-sidebars ...">
no '#type' => 'page':
<body class="layout-one-sidebar layout-sidebar-first ...">
The cause was found in bartik_preprocess_html() in file bartik.theme

/**
 * Implements hook_preprocess_HOOK() for HTML document templates.
 *
 * Adds body classes if certain regions have content.
 */
function bartik_preprocess_html(&$variables) {
  // Add information about the number of sidebars.
  if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) {
    $variables['attributes']['class'][] = 'layout-two-sidebars';
  }
  elseif (!empty($variables['page']['sidebar_first'])) {
    $variables['attributes']['class'][] = 'layout-one-sidebar';
    $variables['attributes']['class'][] = 'layout-sidebar-first';
  }
  elseif (!empty($variables['page']['sidebar_second'])) {
    $variables['attributes']['class'][] = 'layout-one-sidebar';
    $variables['attributes']['class'][] = 'layout-sidebar-second';
  }
  else {
    $variables['attributes']['class'][] = 'layout-no-sidebars';
  }

  if (!empty($variables['page']['featured_top'])) {
    $variables['attributes']['class'][] = 'featured-top';
  }

}
Fabianx’s picture

What you want is a theme suggestion:

#type => 'page',
#theme => array('page__mytheme'),

Then use page--mytheme.html.twig

That will ensure that hook_preprocess_page() continues to be called.

Mark_L6n’s picture

Thanks again!
So would it be accurate to say that there are 3 ways to create the basic content of a render array?

1) use a #type that has already been defined
a #type typically provides a default value for #theme [does it always?]
you can specify a value for #theme that overrides the default value
2) assign a string of HTML to #markup
You can specify '#type' => 'markup', or it will automatically be provided if you have #markup defined
3) use your own twig template, specified by '#theme' => 'mytheme' (or '#theme' => array('mytheme'?) and by not setting #type.
It would make sense to treat this in the same way as '#markup' and provide default value like '#type' => 'template', but I don't think this is done.

Fabianx’s picture

Not really,

- #type can also have a #pre_render call that sets #markup. (e.g. #type => link)
- #markup is a special case, yes
- #theme corresponds to things defined via hook_theme()

This all has almost not been changed from D7.

See also:

https://www.drupal.org/node/930760

joelpittet’s picture

@Mark_L6n with all this detail maybe you can update the theme docs to make them more clear for yourself and others?

Mark_L6n’s picture

@joelpittet Yes, hopefully I can write up the basics in a simple way! Hopefully you guys can proofread it here—each time I think I'm making progress understanding it there's something I'm still missing ;-)

Mark_L6n’s picture

Below is a sample template for creating the 3 types of render arrays outlined in post #7. (I'm not following the comment in post #8 "#type can also have a #pre_render call that sets #markup. (e.g. #type => link)"; I'll ask about that below. ) I'd like to suggest something along these lines to the Drupal Console project. Does this look accurate for what it covers?

class DemoController extends ControllerBase {
  /**
   * Dostuff.
   *
   * @return array
   *   Render array
   * Choose one of the 3 create/return render array methods below, and delete the others.
   */
  public function dostuff() {

    // Render array for a Drupal entity type
    // '#type' => 'page'  (page, view, label, fieldgroup, fieldset, link, pager, etc.)  Available types listed at:
    //    https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Annotation%21RenderElement.php/class/RenderElement/8
    //      click on X 'classes are annotated with RenderElement'
    // Types will be documented, see: https://www.drupal.org/node/1617948
    // optional:  '#theme' => 'page__mytheme'
    //   The type will use its own twig template if defined (at core/modules/system/templates, although your theme may override it)
    //   For ex., '#type' => 'page' will use page.html.twig
    //   If you specify a twig file with #theme, it will override the default twig file.
    //   Place the file in [mytheme]/templates/page--mytheme.html.twig
    //   Use '__' syntax (ex: page__mytheme, which converts to file name page--mytheme.html.twig), which allows 
    //   Drupal's theme suggestion mechanism to override these files.
    $render_array = [
      '#type' => 'page',
      // '#theme' => 'page__mytheme',
    ];
    return $render_array;


    // Render array that specifies a Twig file for returning custom markup, using the #theme property 
    $render_array = [
      '#theme' => 'page__mytheme',
    ];
    return $render_array;

    // Render array using the #markup property to return custom HTML/string
    // #type property is optional, but must be 'markup' if specified. (If missing, '#type' => 'markup' will be added automatically).
    // This type of render array is meant to be used with only short and simple markup strings.  For larger markup strings, use the option
    // that specifies a Twig file with the #theme property.
    return [
        '#type' => 'markup',
        '#markup' => '<p>' . $this->t('Implement method: dostuff') . '</p>',
    ];
  }
Mark_L6n’s picture

I'm not following, from post #8

- #type can also have a #pre_render call that sets #markup. (e.g. #type => link)

1) Will '#type' => 'link' and #markup co-exist on the same page?
If so, what happens then? Is the markup only part of the rendered HTML? Which part (i.e. between <a> and </a>, inside <a .....> etc.)?
2) Could this be specified directly, without using #pre_render? If not, why not?

Mark_L6n’s picture

It appears there (for now) is a 4th way to create a render array, using '#type' => 'inline_template' along with '#template' => <Twig code> :

  '#type' => 'inline_template',
  '#template' => '{{ error }} {{ description }}',    <-- Twig code
  '#context' => array(                               <-- (opt.) Variables passed from PHP into the Twig code
    'error' => $error,
    'description' => $description,
  ),

(Ex. from core/modules/system/system.install. For more examples, run from the Drupal root egrep -ir "inline_template" . )
However, this method appears to be under discussion at #2273925: Ensure #markup is XSS escaped in Renderer::doRender().
Thus, it appears you can currently call Twig code:

  1. Inline, using this method.
  2. Externally from a file specified in '#theme' and leaving #type undefined. (See post #7 3))
Mark_L6n’s picture

Answers to questions in post #12, which were referring to this quote from post #8:

- #type can also have a #pre_render call that sets #markup. (e.g. #type => link)

Found the code referred to in core/lib/Drupal/Core/Render/Element/Link.php, which has function function preRenderLink() and a render array referring to it: '#pre_render' => array( array($class, 'preRenderLink'), ), An example render array produced for a link is:

$element['#type']=link
$element['#markup']=<a href="/en/user/logout">Log out</a>

1) Will '#type' => 'link' and #markup co-exist on the same page? Yes.

If so, what happens then? It appears that whatever is in #markup, regardless of what is elsewhere, is regarded as the rendered HTML of the render array. In fact, when a render array is rendered into HTML, the result is placed in #markup. To save processing time, if something is in #markup, that render array will not be rendered again, with #markup being the final rendered HTML (unless #post_render functions modify it). (Could someone verify that all parts of this answer are correct?)

Is the markup only part of the rendered HTML? No, it is all of the rendered HTML. Follow-up Q: will #prefix/#suffix get added to this, and is this different in D7 and D8?

Which part (i.e. between <a> and </a>, inside <a .....> etc.)? Q not applicable, as #markup is all of the rendered HTML.

2) Could this be specified directly, without using #pre_render? If not, why not? Yes. From the following render array, the #markup is returned as is, despite #type => link:

$render_array = [
  '#type' => 'link',
  '#markup' => '<p>' . $this->t('Test Markup') . '</p>',
];

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.