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/drupalSettingsCC 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>';
<script>alert("I am evil!");
</script>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 NULLTwig 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:
<strong>This is an example</strong>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