Inline JavaScript in Drupal 8
Tags: drupal
Published: 2019-11-20 00:49:27

## 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.

![](https://thepracticaldev.s3.amazonaws.com/i/o5fxn2yycpr1d8twk52j.gif)

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](https://www.drupal.org/docs/8/core/modules/dynamic-page-cache/overview), 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](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) vulnerabilities can be substantially reduced by blocking inline scripts with a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://www.drupal.org/project/csp).


## 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](https://www.drupal.org/project/ga) 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:

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

### 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:

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

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

```

### 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.

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

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


## 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](https://www.drupal.org/project/attachinline) 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](https://www.drupal.org/project/csp) to limit the risk of using inline scripts.

### Usage

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

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

    // New functionality
    'js' => [
      'alert("World");',
      [
        'data' => 'alert("Hello!")',
        'scope' => 'header',
        'weight' => -10,
      ],
    ],
  ],
];
```

### 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.