What's New in

D8 Theming

drupal.org: mikeker

Me: circa 2013

  • Avoid template functions
    Use Twig templates instead

// Theme a table in D7:
$variables['my_table'] = theme('table', array(
  'header' => $header,
  'rows' => $rows,
  'attributes' => array('id' => 'my-module-table')
));

// Theme a table in D8:
$variables['my_table'] = [
  '#type' => 'table',
  '#header' => $header,
  '#rows' => $rows,
  '#attributes' => ['id' => 'my-module-table'],
];

Long live render arrays!

Long live Twig!

  • PHPTemplate gave too much control to the template
  • Almost forgot to remove it... Oops
  • Hate Twig? Fine, use your own engine

Everything is a block

  • Page title
  • Branding area
  • Local tasks or actions
  • Messages
  • Breadcrumbs

 

And now blocks don't suck any more!

 

CC BY-NC-ND 2.0 image byblake.thornberry

Theme Structure

  • Themes live in the /themes directory
  • theme.info becomes theme.info.yml
  • template.php becomes *.theme
  • Breakpoints defined in a *.breakpoints.yml file
  • Assets stored in /js, /css, /images directories
  • Templates stored in /template directory

Libraries

  • Defined in a *.libraries.yml file
  • Can define dependencies
  • Are #attached to elements
$build['#attached']['library'][] = 'my_module/cool-slideshow';
$build['#attached']['drupalSettings']['my_module']['cool-slideshow']↩
 ['numSlides'] = 20;
# In my_module/my_module.libraries.yml:
cool-slideshow:
  version: 1.x
  css:
    theme:
      css/cool-slideshow.css
  js:
    js/cool-slideshow.js
  dependencies:
    - core/jquery
    - core/drupalSettings

CC by-nc-sa image by Pekka Nikrus

  • Use #attached instead
  • Usually in a preprocess function

Long live cachability!

Render late,

render once

Attributes

function template_preprocess(&$variables, $hook) {
  $variables['classes_array'] = array(drupal_html_class($hook));
}
function HOOK_preprocess_node(&$variables) {
  $variables['classes_array'][] = 'foo';
}
function template_process(&$variables, $hook) {
  $variables['classes'] = implode(' ', $variables['classes_array']);
}

// In node.tpl.php
<div id="node-<?php print $node->nid; ?>" class="<?php print↩
   $classes; ?> clearfix"<?php print $attributes; ?>>
function template_preprocess(&$variables, $hook, $info) {
  ...
}
function HOOK_preprocess_node(&$variables) {
  $variables['attributes']['class'][] = 'foo';
}

{# In node.html.twig #}
{%
  set classes = [
    'node',
    node.isPromoted() ? 'node--promoted',
    node.isSticky() ? 'node--sticky',
    not node.isPublished() ? 'node--unpublished',
  ]
%}
<article{{ attributes.addClass(classes)↩
  .setAttribute('id', 'node-' ~ node.id) }}>

Stripped down markup

  • Minimal HTML and zero classes
  • Classes are now in the Classy theme
  • Classes are added in the template (not in preprocess functions!)

And that's a good thing

D7: Stark

Field with a single element

<div class="field field-name-field-bef-test-numbers 
            field-type-list-integer field-label-above">
  <div class="field-label">Numbers: </div>
  <div class="field-items">
    <div class="field-item even">Three</div>
  </div>
</div>

D8: Stark

Field with a single element

<div>
  <div>Number</div>
  <div>Three</div>
</div>

D8: Classy

Field with a single element

<div class="field field--name-field-number 
            field--type-list-integer field--label-above">
  <div class="field__label">Number</div>
  <div class="field__item">Three</div>
</div>

Easily remove styling

stylesheet-remove:
  - core/something/not/wanted/in/my/site.css
stylesheets-remove:
  - '@bartik/css/style.css'

Stable

Allows core to evolve while not breaking your site's theme

https://www.flickr.com/photos/classblog/

Debugging

# Uncomment these lines in settings.php
if (file_exists(__DIR__ . '/settings.local.php')) {
  include __DIR__ . '/settings.local.php';
}
# Change this is sites/default/services.yml
parameters:
  twig.config:
    # @default false
    debug: true
$ cp sites/example.settings.local.php ↵ 
 sites/default/settings.local.php
$ drush cache-rebuild  # alias: drush cr
# Uncomment the following line in settings.local.php
$settings['cache']['bins']['render'] = 'cache.backend.null';
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'field' -->
<!-- FILE NAME SUGGESTIONS:
   * field--node--body--page.html.twig
   * field--node--body.html.twig
   * field--node--page.html.twig
   * field--body.html.twig
   x field--text-with-summary.html.twig
   * field.html.twig
-->
<!-- BEGIN OUTPUT from 'core/themes/classy/templates/↵
 field/field--text-with-summary.html.twig' -->

... Template output goes here ...

<!-- END OUTPUT from 'core/themes/classy/templates/↵
 field/field--text-with-summary.html.twig' -->
{% Add this to your template file... %}
<pre>{{ dump(attributes) }}</pre>


<!-- ... get this in your HTML -->
<pre>object(Drupal\Core\Template\Attribute)#1440 (1) {
  ["storage":protected]=>
  array(6) {
    ["data-history-node-id"]=>
    object(Drupal\Core\Template\AttributeString)#1448 (2) {
      ["value":protected]=>
      string(2) "49"
      ["name":protected]=>
      string(20) "data-history-node-id"
    }
    ["data-quickedit-entity-id"]=>
...
$ drush dl devel
$ drush en -y kint
<pre>{{ kint(attributes) }}</pre>

Theme suggestions

// Drupal 7 version
function HOOK_preprocess_foo(&$variables) {
  $variables['theme_hook_suggestions'][] = ↩
   'cool_foo_template';
}
// Drupal 8 version
function HOOK_theme_suggestions_foo_alter( ↩
 array &$suggestions, array $variables) {
  $suggestions[] = 'cool_foo_template';
}
// No more singular -- this does not work in Drupal 8:
$variables['theme_hook_suggestion'] = 'node__' . 'my_suggestion';
// Instead add suggestions to the end of the suggestions array.

Twig

Twig Syntax

{{ Outputs variables }}

 

{% Control (conditionals, loops) %}

 

{# Comments #}

Otherwise, you're just writing HTML

Twig Variables

Auto-escaped by default


$variables['danger'] = 
  '<script>alert("I am evil!");</script>';

&lt;script&gt;alert(&quot;I am evil!&quot;);
  &lt;/script&gt;

Twig Variables

Dot syntax

{{ foo.bar }}
// If foo is an array
$foo['bar']

// If foo is an object
$foo->bar      // Checks for valid property
$foo->bar()    // Checks for valid method
$foo->getBar() // Checks for get method
$foo->isBar()  // Checks for is method
// Returns NULL

Twig Logic

{# use "and" instead of &&, "or" instead of ||, 
  and "not" instead of ! #}

{% if node.isPromoted and node.status %}
  This is really important!
{% else %}
  Not as important or not published
{% endif %}

{# add "b-" to make it a bit-wise operator #}

Twig Comparisons

{# RegEx lite operations #}
{% if 'foo' starts with 'f' %}
  ...
{% endif %}
{% if 'foo' ends with 'o' %}
  ...
{% endif %}

{# Real RegEx parsing via match #}
{% if 'foo' matches '/^f(o)*$/' %}
  ...
{% endif %}

Twig

Handling whitespace

<pre>
  {% if 'foo' matches '/^f(o)*$/' %}
    This is foo.
  {% endif %}
</pre>

Results in:

<pre>      This is foo.
  </pre>
<pre>
{% spaceless %}
  {% if 'foo' matches '/^f(o)*$/' %}
    This is foo.
  {% endif %}
{% endspaceless %}
</pre>

Results in:

<pre>This is foo.</pre>
<pre>
  {%- if 'foo' matches '/^f(o)*$/' -%}
    This is foo.
  {%- endif %}
</pre>

Also results in:

<pre>This is foo.</pre>

Twig Syntax

Filters

Twig Filters

Modify variables using the pipe delimiter

{{ page_title|title }}

{{ (numFoo - numBar)|abs }}
{{ node_title|striptags|title }}
{{ items|safe_join(', ') }}

Twig Filters

Block style

{% filter escape %}
  <strong>This is an example</strong>
{% endfilter %}

Results in:

&lt;strong&gt;This is an example&lt;/strong&gt;

Twig Filters in Drupal

  • t
  • without
  • clean_class/clean_id
  • render
  • format_date
  • safe_join
  • drupal_escape

From Twig

From Drupal

http://twig.sensiolabs.org/doc/filters/index.html

Twig Filters

{{ content|without('field_example';) }}
{# Print other stuff... #}
{{ content.field_example }}
{% set class = 'node--type-' ~ node.bundle|clean_class %}
...
<article{{ attributes.addClass(class) }}>
<a href="{{ url('<front>') }}" title="{{ Home|t }}">

Twig Functions

(They look a lot like filters...)

Twig Functions

  • render_var
  • url
  • path
  • link
  • file_url
  • attach_library
  • active_theme_path
  • active_theme

From Twig

From Drupal

http://twig.sensiolabs.org/doc/functions/index.html

Examples

<pre>{{ dump(someVariable) }}</pre>
<pre>{{ dump() }} </pre>
{{ 'now'|date(constant('DATE_RSS')) }}
{# Can also pull constants from objects #}
{{ 'now'|date(constant(
  'Drupal\\SomeDateObject::MY_DATE_FORMAT') }}
{# Both maintain the default value for the format param #}
{{ 'now'|date(null, timezone='Europe/Paris') }}
{{ 'now'|date(timezone='Europe/Paris') }}

Twig Control

Conditionals, loops, etc.

Loops

{% set test = ['foo', 'bar', 'baz'] %}
<pre>
{% for item in test %}
  {{ item }}
{% endfor %}
</pre>

Results in:
<pre>  foo
  bar
  baz
</pre>

Loops

{% set test = ['foo', 'bar', 'baz'] %}
<pre>
{% for item in test %}
  {{- loop.index }} out of {{ loop.length }} - {{ item }}
{% endfor %}
</pre>

Results in:
<pre>1 out of 3 - foo
2 out of 3 - bar
3 out of 3 - baz
</pre>

Magic loop variable

Loops

{% set test = ['foo', 'bar', 'baz'] %}
<pre>
{% for item in test if item starts with 'b' -%}
  Begins with b item #{{ loop.index }}: {{ item }}
{% endfor %}
</pre>

Results in:
<pre>Begins with b item #1: bar
Begins with b item #2: baz
</pre>

for-if loops

Loops

{% set foo = []; %}
<pre>
{% for item in foo %}
  {{- loop.index }} out of {{ loop.length }} - {{ item }}
{% else %}
  Nothing to loop on.
{% endfor %}
</pre>

Results in:
<pre>  Nothing to loop on.
</pre>

for-else loops

{% set foo = [] %}
<pre>
{% if foo is defined and foo is not empty %}
  {% for item in foo %}
    {{- loop.index }} out of {{ loop.length }} - {{ item }}
  {% endfor %}
{% else %}
  Nothing to loop on.
{% endif %}
</pre>

Template Inheritance

The Drupal 7 way:

  • Copy/Paste the template
  • Cut yourself off from future changes

The Drupal 8 way

{# Super simplified block.html.twig #}
<div{{ attributes }}>
  {% block label %}
    <h2{{ title_attributes }}>{{ label }}</h2>
  {% endblock %}
  {% block content %}
    {{ content }}
  {% endblock %}
</div>
{# Child template: myblock.html.twig #}
{% extends block.html.twig %}
{% block content %}
  <p>{{ content }}</p>
{% endblock %}
{% block label %}
  {{ parent() }}
  <h3>Some sorta sub-title</h3>
{% endblock %}
{# New shiny block.html.twig #}
<block{{ attributes }}>
  {% block label %}
    <blabel {{ title_attributes }}>{{ label }}</blabel>
  {% endblock %}
  {% block content %}
    {{ content }}
  {% endblock %}
</div>

Any questions?

Drupal 8 Theming

By Mike Keran

Drupal 8 Theming

  • 1,091