DEV Community

Geoff
Geoff

Posted on

Inline JavaScript in Drupal 8

A Brief History Inline

In Drupal 7, the global function drupal_add_js() accepted three types of arguments:

  • A path to an internal file, or an external URL
  • A snippet of JavaScript to be added to the page inline
  • An array of values to be added to Drupal.settings for JavaScript to use.

Each item had a few properties to determine where on the page it appeared, and what order relative to other items (e.g. ['scope' => 'header', 'group' => JS_DEFAULT, 'weight' => -10]).

drupal_add_js() could be called at any time during page execution, prior to the page markup being rendered, or render arrays could have the same parameters added to their ['#attached']['js'] array.

The Drupal 8 Libraries API

Drupal 8 introduced some substantial changes:

  • Instead of adding individual files to the page, modules and themes must define libraries - which can contain multiple files.
  • Libraries can only be added to the page by specifying them in a render element's ['#attached']['library'] array, or via hook_page_attachments().
  • There is no API to add inline snippets of JavaScript to the page.

The Libraries API offers a number of benefits:

  • If there was an ordering conflict with JavaScript files in D7, a site would need to implement an alter function to adjust the weights of relevant files to ensure the correct execution order. Drupal 8 libraries instead each define what other libraries they depend on, allowing Drupal core to automatically resolve the order.
  • In D7 all scripts defaulted to being added to the head of the page, since it wasn't possible to determine if libraries like jQuery did not have any other scripts placed in the header dependent on it. By having D8 libraries define their dependencies, they can default to be included at the bottom of the page markup for better loading performance, but automatically be moved to the header if needed by a dependent library.
  • Drupal 8 introduced the Dynamic Page Cache, which caches rendered fragments of the page and improves performance for authenticated users. If scripts were added separately from render elements, the Dynamic Page Cache wouldn't be able to properly add them when content is retrieved from the cache, resulting in broken functionality.
  • The risk of Cross-Site Scripting vulnerabilities can be substantially reduced by blocking inline scripts with a Content Security Policy. If a site's modules all make use of the libraries API & drupalSettings, it's even possible to automatically generate a policy for a Drupal site.

Adding Inline Snippets

If you think you really need some inline JavaScript it's still possible to add it to a page in a few ways, depending on your needs.

Copy to an external file

If the snippet is static code, and any of it's configuration is included in the snippet and doesn't change based on the page content, it can just be placed in an external file added to the page via the Libraries API. The code will then be aggregated with the page's other JS.

Refactor to use DrupalSettings

When data needs to be passed from PHP to JS the best solution is to refactor the snippet to work as an external file that uses data provided by drupalSettings.

The Googalytics module is an example of this approach. Instead of using string concatenation to generate JavaScript to be added as an inline snippet, the method parameters are added to drupalSettings which is added to the page as a JSON object, then dynamically passed to the relevant JS function:



  for (var i = 0; i < drupalSettings.ga.commands.length; i++) {
    ga.apply(this, drupalSettings.ga.commands[i]);
  }


Enter fullscreen mode Exit fullscreen mode

Create a new render element

If the snippet is a repeatable element, create a new renderable element so that you can place it on the page via a render array, and provide any parameters that need to passed to the corresponding Twig template:



$render['js_embed'] = [
  '#type' => 'js_embed',
  '#id' => 12345,
];


Enter fullscreen mode Exit fullscreen mode


<script>
  js_embed( {{ id }} );
</script>



Enter fullscreen mode Exit fullscreen mode

Add to html.html.twig

Snippets can be added directly to Twig templates. For global JavaScript, just add it to html.html.twig after <js-placeholder token="{{ placeholder_token }}"> or <js-bottom-placeholder token="{{ placeholder_token }}"> as required. If configurable parameters are needed, these can be passed through to the twig template by setting them in hook_preprocess_html().

HTML Markup

A script tag can be added to the page via the html_tag render element.



$render['snippet'] => [
  '#type' => 'html_tag',
  '#tag' => 'script',
  '#value' => 'alert("Hello World!");',
];


Enter fullscreen mode Exit fullscreen mode

or via #markup



$render['snippet'] => [
'#markup' => '<script>alert("Hello World!");</script>',
'#allowed_tags' => ['script'],
];

Enter fullscreen mode Exit fullscreen mode




The Attach Inline Module

Some people still want core to provide an API to add inline snippets. At the risk of bolstering those who want this re-introduced to core (which I don't think is a good idea), I created the Attach Inline module to:

  • Explore whether a contrib module can sufficiently supply this functionality.
  • Better scope the effort required to implement a robust API.
  • Enumerate the limitations and trade-offs of this API, to support whether or not it should actually be a candidate for re-inclusion in Drupal core.
  • Provide an integration with the Content Security Policy module to limit the risk of using inline scripts.

Usage

The module re-introduces the ['#attached']['js'] array to render elements:



$render['element'] = [
'#attached' => [
// Existing Functionality
'library' => [
'drupal/drupalSettings'
],
'drupalSettings' => ['module' => $data],

<span class="c1">// New functionality</span>
<span class="s1">'js'</span> <span class="o">=&gt;</span> <span class="p">[</span>
  <span class="s1">'alert("World");'</span><span class="p">,</span>
  <span class="p">[</span>
    <span class="s1">'data'</span> <span class="o">=&gt;</span> <span class="s1">'alert("Hello!")'</span><span class="p">,</span>
    <span class="s1">'scope'</span> <span class="o">=&gt;</span> <span class="s1">'header'</span><span class="p">,</span>
    <span class="s1">'weight'</span> <span class="o">=&gt;</span> <span class="o">-</span><span class="mi">10</span><span class="p">,</span>
  <span class="p">],</span>
<span class="p">],</span>
Enter fullscreen mode Exit fullscreen mode

],
];

Enter fullscreen mode Exit fullscreen mode




The limitations so far

  • Because inline snippets cannot define their library dependencies directly, if a snippet is added to the page header the dependency resolution cannot determine what libraries should be promoted to the page header. This could be worked around by having a placeholder library that only defines the dependencies which inline snippets need to be placed in the page head.
  • Snippets only have basic weighted ordering, and are placed after all included files. If an external file requires a snippet to be defined first, this can currently only be accomplished by placing the snippet in the page head and the corresponding library at the end of the page.
  • I have no idea if this cooperates with the Dynamic Page Cache or Big Pipe modules, or many other pieces of Drupal core.

Top comments (0)