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

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

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


 &lt;em&gt;Kittens&lt;/em&gt;

<em>Kittens</em>

Prints in the browser

What is Autoescaping?


  {{ text }}

Prints markup in HTML


 &lt;em&gt;Kittens&lt;/em&gt;

<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

Made with Slides.com