Theming deep dive
Lauri Eskola (lauriii)
Twitter: @laurii1
https://slides.com/lauriii/ddd-16
Lauri Eskola
- Drupal Theme System co-maintainer
- Working for Druid
- I love kittens (added 78 of them to Drupal 8 core and counting...)
- ... and I like to break Bartik.
Twitter: @laurii1
Drupal 8 Theme System pipeline
Code examples:
Drupal 8 Theme System pipeline
hook_theme() and build your render array
<?php
/**
* Implements hook_theme().
*/
function sandwich_theme() {
return [
'sandwich' => [
'variables' => [
'attributes' => [],
'name' => '',
'bread' => '',
'cheese' => '',
'veggies' => [],
'protein' => '',
'condiments' => [],
],
],
];
}
<?php
public function build() {
return [
'#theme' => 'sandwich',
'#name' => $this->t('Chickado'),
'#attributes' => [
'id' => 'best-sandwich',
],
'#bread' => $this->t('Sourdough'),
'#cheese' => $this->t('Gruyère'),
'#veggies' => [
$this->t('Avocado'),
$this->t('Red onion'),
],
'#protein' => $this->t('Chicken'),
'#condiments' => [
$this->t('Mayo'),
$this->t('Dijon'),
],
'#attached' => [
'library' => [
'sandwich/flavour'
]
],
];
}
Drupal 8 Theme System pipeline
Theme suggestions
node.html.twig
node--[type].tpl.php
hook_theme_suggestions and hook_theme_suggestions_alter
<?php
/**
* Implements hook_theme_suggestions_sandwich().
*/
function sandwich_theme_suggestions_sandwich($variables) {
return 'sandwich__' . strtolower($variables['name']);
}
/**
* Implements hook_theme_suggestions_sandwich_alter().
*/
function sandwich_theme_suggestions_sandwich_alter(&$suggestions, $variables) {
}
sandwich--chickado.html.twig
sandwich--yummy.html.twig
Debugging theme suggestions
sites/default/services.yml
parameters:
twig.config:
debug: true
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'node' -->
<!-- FILE NAME SUGGESTIONS:
* node--article--teaser.html.twig
* node--article.html.twig
* node--teaser.html.twig
x node.html.twig
-->
<!-- BEGIN OUTPUT from 'core/themes/bartik/templates/node.html.twig' -->
Will result into this:
Drupal 8 Theme System pipeline
template_preprocess_HOOK and
hook_preprocess_HOOK
<?php
/**
* Implements template_preprocess_sandwich().
*/
function template_preprocess_sandwich(&$variables) {
$variables['name'] = 'Kitten';
}
/**
* Implements hook_preprocess_sandwich().
*/
function sandwich_preprocess_sandwich(&$variables) {
$variables['name'] = 'Llama';
}
/**
* Implements hook_preprocess_sandwich__chickado().
*/
function sandwich_preprocess_sandwich__chickado(&$variables) {
$variables['name'] = 'Flamingo';
}
Preprocess functions are good for modifying existing content, and they are not supposed to add new variables
Converting a preprocess function into a template
Drupal 8 Theme System pipeline
sandwich.html.twig
<section{{ attributes.addClass('sandwich') }}>
<h2>{{ name }}</h2>
{% if bread %}
<p><strong>Bread:</strong> {{ bread }}</p>
{% endif %}
{% if protein %}
<p><strong>Protein:</strong> {{ protein }}</p>
{% endif %}
{% if cheese %}
<p><strong>Cheese:</strong> {{ cheese }}</p>
{% endif %}
{% if veggies %}
<strong>Veggies:</strong>
<ul>
{% for veg in veggies %}
<li>{{ veg }}</li>
{% endfor %}
</ul>
{% endif %}
{% if condiments %}
<strong>Accoutrement:</strong>
<ul>
{% for condiment in condiments %}
<li>{{ condiment }}</li>
{% endfor %}
</ul>
{% endif %}
</section>
Render system
Build your render array with render element
<?php
public function build() {
return [
'#type' => 'sandwich',
'#name' => $this->t('Chickado'),
'#attributes' => [
'id' => 'best-sandwich',
],
'#cheese' => $this->t('Gruyère'),
'#veggies' => [
$this->t('Avocado'),
$this->t('Red onion'),
],
'#protein' => $this->t('Chicken'),
];
}
<?php
/**
* Provides a render element for displaying some delicious sandwiches.
*
* @RenderElement("sandwich")
*/
class Sandwich extends RenderElement {
public function getInfo() {
return [
'#theme' => 'sandwich',
'#attached' => [
'library' => [
'sandwich/flavour'
],
],
'#bread' => $this->t('Sourdough'),
'#condiments' => [
$this->t('Mayo'),
$this->t('Dijon'),
],
];
}
}
Render array passed to theme system
[
'#theme' => 'sandwich',
'#name' => $this->t('Chickado')
'#attributes' => [
'id' => 'best-sandwich',
],
'#cheese' => $this->t('Gruyère'),
'#veggies' => [
$this->t('Avocado'),
$this->t('Red onion'),
],
'#protein' => $this->t('Chicken'),
'#attached' => [
'library' => [
'sandwich/flavour'
],
],
'#bread' => $this->t('Sourdough'),
'#condiments' => [
$this->t('Mayo'),
$this->t('Dijon'),
],
];
Creating a pre_render callback
<?php
/**
* Provides a render element for displaying some delicious sandwiches.
*
* @RenderElement("sandwich")
*/
class Sandwich extends RenderElement {
public function getInfo() {
return [
'#theme' => 'sandwich',
'#name' => '',
'#pre_render => [
[$this, 'preRenderSandwich'],
],
];
}
public static function preRenderSandwich($element) {
if ($element['#name'] == 'chickado') {
$element['#condiments'][] = t('Mustard');
}
return $element;
}
}
Creating a pre_render callback
<?php
/**
* Implements hook_element_info_alter().
*/
function sandwich_element_info_alter(array &$types) {
// Decrease the default size of textfields.
if (isset($types['sandwich'])) {
$types['sandwich']['#pre_render'][] = 'sandwich_pre_render_callback';
}
}
function sandwich_pre_render_callback($element) {
...
return $element;
}
Download the code:
Future plans
Make theme system component based:
https://www.drupal.org/node/2702061
Supersede Backbone in core admin UIs with a new client-side framework
Basics of Twig
The “Say Something” Syntax: {{ ... }}
The “Do Something” Syntax: {% ... %}
The Comment Syntax: {# ... #}
Printing variables using Twig magic
{{ sandwich.cheese }}
// Array key.
$sandwich['cheese'];
// Object property.
$sandwich->cheese;
// Also works for magic get (provided you implement magic isset).
$sandwich->__isset('cheese'); && $sandwich->__get('cheese');
// Object method.
$sandwich->cheese();
// Object get method convention.
$sandwich->getCheese();
// Object is method convention.
$sandwich->isCheese();
// Method doesn't exist/dynamic method.
$sandwich->__call('cheese');
Sounds slow?
If you need to render a lot of Twig files, install Twig PHP package
Debugging variables
{{ kint(node) }}
Twig filters
Meant to manipulate a variable. Takes the first parameter from the variable before "|".
{% set text = 'Kitten' %}
{# Print variable using length filter. #}
{{ text|length }}
Example
Returns
6
Twig functions
Functions in Twig are like PHP functions
{{ attach_library('color/drupal.color') }}
Example
Print what you want, when you want
{# We give you what you ask for. #}
{{ content|without('comments', 'links') }}
Drupal 7
Drupal 8
<?php
// We hide the comments and links now so that we can render them later.
hide($content['comments']);
hide($content['links']);
print render($content);
Avoid duplication
page.html.twig
...
{% block title %}
<h2{{ title_attributes }}>
<a href="{{ url }}" rel="bookmark">{{ label }}</a>
</h2>
{% endblock %}
...
page--front.html.twig
{% extends "@classy/page.html.twig" %}
{% block title %}
<h1{{ title_attributes }}>
<a href="{{ url }}" rel="bookmark">{{ label }}</a>
</h1>
{% endblock }
Macros
{% import _self as elements %}
{% if items|count > 1 %}
<div>
{{ elements.list(items) }}
</div>
{% else %}
<span>
{{ elements.list(items) }}
</span>
{% endif %}
{% macro list(items) %}
{% if items %}
{% for item in items %}
<div>{{ item }}</div>
{% endif %}
{% endif %}
{% endmacro %}
Markup should live inside a Twig template. Not in PHP!
Rule #1
Autoescape
What is escaping?
<?php
use Drupal\Component\Utility\Html
print Html::escape('<em>Kittens</em>');
Prints markup in HTML
<em>Kittens</em>
<em>Kittens</em>
Prints in the browser
What is Autoescaping?
{{ text }}
Prints markup in HTML
<em>Kittens</em>
<em>Kittens</em>
<?php
function bartik_preprocess_page(&$variables) {
$variables['text'] = '<em>Kittens</em>';
}
Prints in the browser
Why do we need autoescaping?
Why do we need autoescaping?
How to avoid autoescaping?
{{ text }}
Prints markup in HTML
<em>Kittens</em>
Kittens
<?php
function bartik_preprocess_page(&$variables) {
$variables['text']['#markup'] = '<em>Kittens</em>';
}
Prints in the browser
How to avoid autoescaping?
{{ text }}
Prints markup in HTML
<em>Kittens</em>
Kittens
<?php
use Drupal\Component\Render\FormattableMarkup;
function bartik_preprocess_page(&$variables) {
$variables['text'] = new FormattableMarkup('<em>@txt</em>', ['@txt' => 'Kittens']);
}
Prints in the browser
But it has also its caveats...
When autoescaped strings are safe?
Whenever the escaped string is being printed in HTML node.
Attributes are NOT HTML!
<?php
use Drupal\Component\Render\FormattableMarkup;
new FormattableMarkup('<em@txt></em>', ['@txt' => 'Kittens']);
new FormattableMarkup('<a href="@url"></a>', ['@url' => 'http://kittens.com']);
new FormattableMarkup('<a href="@url"></a>', [
'@url' => 'javascript:alert(String.fromCharCode(88,83,83))'
]);
These are all dangerous:
New placeholder for URLs
<?php
use Drupal\Component\Render\FormattableMarkup;
new FormattableMarkup('<a href=":url"></a>', [
':url' => 'javascript:alert(String.fromCharCode(88,83,83))'
]);
This is safe:
Array keys
<?php
use Drupal\Core\StringTranslation\TranslatableMarkup;
// These two do the same thing.
$text = new TranslatableMarkup('Translate me');
$text = t('Translate me');
$array[$text] = $text;
<?php
use Drupal\Core\StringTranslation\TranslatableMarkup;
$text = new TranslatableMarkup('Translate me');
$array[(string) $text] = $text;
This will fatal:
This works:
Autoescaping is only enabled for Twig templates
Which means using any custom templating engine or Theme functions are not autoescaped
That's why PHPTemplate was overtaken by the amazing Nyan Cat templating engine.
https://www.drupal.org/node/2575199
Questions?
Twitter: @laurii1
Drupal Dev Days 2016 - Theming deep dive
By lauriii
Drupal Dev Days 2016 - Theming deep dive
- 1,402