Simple Drupal 8 HTML 5 video field formatter

22 October 2016

Great!!! So you started working with Drupal 8 and fancy taking a shot at making your own custom field formatter. If you've been working with Drupal 7 for a while the changes to D8 can a be a bit of a shock to the system, but it doesn't have to be a completely daunting endeavor. The idea of this blog post is to help developers just starting out with D8 get their feet wet by making a pretty simple, but pretty cool, module.

Ok, so what does this module do? The module will allow you to add multiple video files in a single field as <source> to format and display a beautiful HTML 5 <video> to your page. I have also added autoplay functionality as a setting on the field to play on page load. Once you get the module up and going have a play around and see if you can extend the settings on the field to add further functionality.

So let's get to it!!

.info.yml

Every great module has a great .info.yml file. That goes without saying as the .info.yml file is the only required file for a D8 module.

html5_video/html5_video.info.yml

name: HTML5 Video
type: module
description: HTML5 Video field formatter for file fields.
core: 8.x
package: Custom
dependencies:
  - file

Note: make sure you're using 2 spaces, not tabs, or 1 space when nesting in .yml files otherwise it'll just stop working. It's a real stickler for those 2 spaces. Weird I know!

Tell Drupal to use a template

Much the same as D7 when declaring a theme template, we still declare the theme template in the same way. However, as you can see we do not need to define our template in hook_theme, D8 will search the module templates directory for video-tag.html.twig.

html5_video/html5_video.module

<?php

/**
 * @file
 * Contains html5_video.module.
 */

use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Implements hook_help().
 * 
 * @param string $route_name
 *   For page-specific help, use the route name as identified in the module's
 *   routing.yml file.
 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
 *   The current route match.
 *
 * @return mixed
 *   A render array, localized string, or object that can be rendered into a
 *   string, containing the help text.
 */
function html5_video_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    // Main module help for the html5_video module.
    case 'help.page.html5_video':

      $output = [
        '#type' => 'container',
        '#attributes' => [
          'class' => [
            'help-wrapper'
          ],
        ],
        'title' => [
          '#type' => 'html_tag',
          '#tag' => 'h3',
          '#value' => t('About'),
        ],
        'body' => [
          '#type' => 'html_tag',
          '#tag' => 'p',
          '#value' => t('HTML5 video field formatter for file fields.'),
        ]
      ];

      return $output;

    default:
  }
}

/**
 * Implements hook_theme().
 *
 * @param array $existing
 *   An array of existing implementations that may be used for override
 *   purposes.
 * @param array $type
 *   Whether a theme, module, etc. is being processed. This is primarily useful
 *   so that themes tell if they are the actual theme being called or a parent
 *   theme.
 * @param string $theme
 *   The actual name of theme, module, etc. that is being being processed.
 * @param string $path
 *   The directory path of the theme or module, so that it doesn't need to be
 *   looked up.
 *
 * @return array
 *   An associative array of information about theme implementations.
 */
function html5_video_theme($existing, $type, $theme, $path) {
  return [
    'video_tag' => [
      'variables' => [
        'sources' => NULL,
        'autoplay' => NULL,
      ],
    ],
  ];
}

Note: The preferred syntax for D8 arrays is square brackets [] opposed to the array().

Rendering in Twig template

In D7 we would render our render arrays into a string and pass in our variables to our template but now in D8 we want to leave rendering as late as possible and have it render in the twig template. We do this so the data can be altered at any point right up until rendering on the template.

html5_video/templates/video-tag.html.twig

<video controls preload="metadata" {{ autoplay }}>
  {% for source in sources %}
    <source src="{{ source.src }}" type="{{ source.mimetype }}"/>
  {% endfor %}
</video>

Classy

For this simple formatter all we really need to do is set our namespace, set our @FieldFormatter annotation, extend FormatterBase class and redefine the viewElememt method. But we're cooler than that and want to add an autoplay setting on the field using the 3 settings methods defaultSettings, settingsForm and settingsSummary all of which are pretty self explanatory.

html5_video/src/Plugin/Field/FieldFormatter/Html5VideoFieldFormatter.php

<?php

/**
 * @file
 * Contains \Drupal\html5_video\Plugin\Field\FieldFormatter\Html5VideoFieldFormatter.
 */

namespace Drupal\html5_video\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Plugin implementation of the 'html5video_field_formatter' formatter.
 *
 * @FieldFormatter(
 *   id = "html5video_field_formatter",
 *   label = @Translation("HTML5 Video"),
 *   field_types = {
 *     "file"
 *   }
 * )
 */
class Html5VideoFieldFormatter extends FormatterBase {
  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'autoplay' => 0,
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $element = parent::settingsForm($form, $form_state);

    $element['autoplay'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Autoplay video'),
      '#default_value' => $this->getSetting('autoplay'),
    ];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $settings = $this->getSettings();
    $has_autoplay = $this->t('No');

    if ($settings['autoplay']) {
      $has_autoplay = $this->t('Yes');
    }

    // Implement settings summary.
    $summary[] = $this->t('This field has autoplay: !has_autoplay', ['!has_autoplay' => $has_autoplay]);

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    if ($this->getSetting('autoplay')) {
      $autoplay = 'autoplay';
    }

    // Render all field values as part of a single <video> tag.
    foreach ($items as $delta => $file) {
      $file = $file->entity;

      // Get URL.
      $uri = $file->uri->value;
      $url =  file_create_url($uri);

      // Get the mime type.
      $mimetype = $file->filemime->value;

      $sources[] = [
        'src' => $url,
        'mimetype' => $mimetype,
      ];
    }

    // Return everything in an array for theming.
    return [
      '#theme' => 'video_tag',
      '#sources' => !empty($sources) ? $sources : [],
      '#autoplay' => !empty($autoplay) ? $autoplay : '',
    ];
  }
}

What to expect

HTML 5 video player