D8 JS Fundamentals

Todd Zebert @toddzebert

Lead Web Dev

Miles @meetmiles

v1.1.0

D8 Integrations

From Not-Invented-Here to Proudly-Found-Elsewhere

Drupal 8 includes many "best of breed" integrations: Symfony, Twig, PSR-0, Composer and more, but notably these JavaScript libraries:

  • Underscore.js - functional programming helpers
  • Backbone.js - a MV* framework with a RESTful JSON interface

We won't cover those in this presentation, but we will cover some Functional Programming concepts.

v1.0.1

Loading JavaScript in D8

Drupal 8 now exclusively uses a "libraries" concept. This is a conceptual extension of D7's hook_library() and the D7 Libraries API Module.

Libraries are defined in YAML format, of course.

You can define one or more libraries in each file.

Libraries are also used for CSS but we'll ignore that for this discussion.

Define a Library

Create or edit the <some-file-name>.libraries.yml file that goes in the root of your theme or module:


this-lib-name:
  js:
    js-path/my-theme.js: {}
						

some-file-name is the machine namee, and can be any name really, but should be the name of your theme or module.

Drupal will aggregate JS files, so if not needed:


    js-path/my-theme.js: { preprocess: false }
						

js-path is relative to the module folder. See PSR-4 namespaces and autoloading in Drupal 8 for more info on pathing.

Library options

You can optionally provide a version number per library - Drupal doesn't use it for anything:


this-lib-name:
  version: "1.0.1"
						

You can also define dependencies per library:


this-lib-name:
  dependencies:
    - core/jquery
		- some_module/some_lib
						

D8 by default loads JS into the footer, to load to header:


this-lib-name:
  header: true
						

Attaching Libraries in a Theme

Reference them in your my-theme-name.info.yml file. They load globally as in on every page.


libraries:
  - core/jquery
  - my-theme-name/this-lib-name
						

Here we're loading our libraries this-lib-name and core/jquery (which is no longer loaded by default).

Attaching Libraries in Twig

You can attach a JS library from a twig template. It will only load if the template loads which means you can conditionally load it based on the naming structure of the Twig template file.


{{ attach_library('my-theme-name/some-other-lib') }}
						

Attaching Libraries in Module PHP

PHP gives you a high degree of control about what libraries are loaded and when.


function mymodule_page_attachments(array &$attachments) {
  $attachments['#attached']['library'][] =
    'mytheme_or_module/some-other-lib';
}
						

Of course you can load any number of libraries in this, and related hooks, that affect the render array.

Attaching Libraries in Theme PHP

If not loading everywhere by using the .yml file.


function mytheme_preprocess_page(&$variables) {
  $variables['#attached']['library'][] =
    'mytheme_or_module/some-other-lib';
}
						

See also related THEME_preprocess_HOOK()'s. Although some discourage using these hooks as they're intended for preprocessing variables.

Conditional Libraries in PHP

To support D8's caching, if you want to *conditionally* attach libraries, you need to provide cacheability metadata*.


function mytheme_or_module_preprocess_page(&$variables) {
  $variables['page']['#cache']['contexts'][] = 'url.path';
  // above line sets the cacheability metadata

  if (\Drupal::service('path.matcher')->isFrontPage()) {
    $variables['#attached']['library'][] =
      'mytheme_or_module/some-other-lib';
  }
}
						

*This is comprised of a combination of up to 3 things: tags, contexts, and max-age. See the Cache API https://www.drupal.org/developing/api/8/cache for more info.

Loading External JavaScript

External "libraries" still have to go inside Drupal libraries.


this-lib-name:
  js:
    https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.js:
      { type: external }

External libs may also have a number of other optional "meta" properties.

Warning: I "broke" the lines for readability, but it's not legal.

Loading External JavaScript, cont.

Since Drupal will minify, if it already is, declare that it is:


{ type: external, minified: true }
						

If attributes are needed in the resulting script line:


      { type: external , attributes: { defer: true, async: true } }

JavaScript Settings

To add "computed" settings or configuration, first you have to define a dependency on core/drupalSettings.


this-lib:
  dependencies:
    - core/jquery
    - core/drupalSettings
						

function mytheme_preprocess_page(&$vars) {
  $vars['#attached']['library'][] = 'mytheme/this-lib';
  $vars['#attached']['drupalSettings']['mytheme']
    ['this-lib']['some-prop'] = $some_var;
}
						

Then access the some-prop setting in JS with:


console.log(drupalSettings.mytheme.this-lib.some-prop);
						

Manipulating Libraries

The theme is able to use two new directives, libraries-extend and libraries-override to extend, remove or replace whole libraries or individual library files.

Both of these are placed in the theme's my-theme.info.yml file.

libraries-extend


libraries-extend:
  other_theme_or_module/some-lib:
    - mytheme/some-other-lib
						

Here, the some-other-lib of the theme is used to extend some-lib of some mytheme_or_module

By "extending" a library it means the new library is always loading with the target library, regardless of conditions.

libraries-override

This directive is more powerful, but when dealing with JS (unlike CSS which may just result in poor layout/design), you can break JavaScript on your site by not paying close attention to JS code dependencies.

You can remove libs:


libraries-override:
  other_theme_or_module/some-lib: false
						

libraries-override, cont.

Or removal of a library file:


libraries-override:
  other_theme_or_module/some-lib:
    js:
      full-path-to-library/some-file.js: false
						

Replacement of libs:


libraries-override:
  other_theme_or_module/some-lib: mytheme/some-other-lib
						

Or replacement of a library file:


libraries-override:
  other_theme_or_module/some-lib:
    js:
      full-path-to-library/some-file.js: path-to-theme-lib/some-other-file.js
						

Inline JavaScript

Almost always JavaScript should be included in a library.

But if you must, put the complete script tag in the html.html.twig file.

Dynamically Altering Libraries

Use this when you want to dynamically specify JS (or CSS) Libraries used across multiple requests.

hook_library_info_alter(&$libraries, $extension)

Allows modules and themes to change libraries' definitions

An example is the Color module which replaces CSS Libraries with a colored CSS libraries.

Dynamically Adding Libraries

To programmatically add an entire library definition this is the hook for you. And they are still cached.

Modules may implement hook_library_info_build() to add dynamic library definitions:

function my_module_library_info_build() {
  $libs = array();
  $libs['my-lib'] = [
    'js' => [
      'my-lib-code.js' => [],
    ],
  ]
}
						

Sneaky JS

While you're suppose to use libraries you can attach into HTML head and create a script tag directly. These are (re)built on every request and so aren't cached and can slow down Drupal, so beware.

If possible, it's best to leave your JS file(s) static and just use JS Settings configurations


$page['#attached']['html_head'][] = [
  [
    '#type' => 'html_tag',
    '#tag' => 'script',
      '#attributes' => [ 'src' => 'some-path/my-file.js' ],
  ], 'my-mode-js', // an identifier for Drupal
];
						

That covers all the ways to get JavaScript into your Drupal sites - what you do with it is a different matter.

What's new with Drupal 8 and JS?

The Drupal 8.4 Edition!

Coding Paradigms

  • Imperative: "you describe how to get what you want", by changing state* and defining OoE**. This includes OO. Ex: JS, PHP, C/++/#, Python, Ruby, etc
  • Declarative: "you describe what you want", ie the desired state result, but not the order of execution. Ex: SQL, Prolog, HTML, CSS
  • Functional: A subset of Declarative. Ex: Haskel, Elm
  • Reactive: A subset of Declarative, that's event-based, concerns data flows and propagation of changes

There are others, and many languages aren't "pure" to a single paradigm.

*OoE: Order of Execution.

Functional Programming, in brief

Disclaimer: this is more about "common" Functional vs "strict" Functional.

Origins of Functional Programming

FP programs are a sequence of stateless function evaluations, and tries to treat programming functions as a mathematical function and less like a simple procedure.

Its roots go back a long way in math, and it's easy to get lost in the arcane math and terms, so let's just keep it simple.

FP Benefits

  • Clarity of code
  • Confidence in the code
  • Testability
  • Code Reuse

FP Concepts

  • First-class and Higher-Order Functions
  • Pure Functions
  • Immutability
  • Function (or Method) Composition
  • Partial Application

FP Functions

FP Functions always take parameter(s)

FP Functions always return a value

First-Class Functions

This means that Functions are treated as "first class citizens" meaning you can assign them to a variable, pass them into other function, or get them return from an function, etc.

JavaScript supports this, as does PHP 5.3 (5.4's support is better), and other languages.

Higher-Order Functions

Functions that operate on other functions, either by taking them as arguments or by returning them, are called higher-order functions.

HO functions abstract over other functions, not just values.

Pure Functions

"Pure FP" languages do not allow Side effects by design.

Side effects are when a procedure changes a variable from outside its scope, or is changed by one.

To avoid this, the output value of a pure function depends only on the arguments that are passed in, and yeilds the same result for the same input every time.

This also greatly helps with code tests!

Pure Functions, cont.

Idempotence is a related term very much based on mathematics, but it simply means calling the same function multiple times with the same input does nothing.

Function Composition

Composition is simply building complex functions by combining simple ones. It is not OO Composition.

When functions are composed by "nesting" (that is, a function with a function within a function, etc), they are evaluated (and "read") from inside to out.

Composition, cont.

Composition is often done with chaining.

"Fluent (Interface)" is the name often given to method cascading AKA chaining. jQuery methods are often chained.

Chaining isn't a FP concept but chaining is more readable, and FP makes it easier.

Immutability

A variable should not change it's value once set.

The important part is not immutability itself, but creating copies of variables and replacing them, instead of mutating the originals.

FP relies heavily on recursion, and eschews loops, among other more arcane concepts, to work in this way.

Immutable "variables" also help avoid errors by preventing accidental changes of things that shouldn't be changed.

ES6 has the const keyword, transpiliers will often ensure mutations throw compiled errors (even if it results in a var); there's libraries that can be used also.

Partial Application

The conceptual goal of PA is it allows you "fill in" arguments to a function "across time" when they are available.

It's a pattern of partially filling a function's parameters with arguments, while others are filled later.

As shown as part of a previous example, it's a way to customize and re-use a function using bind.

Currying is often confused with Partial Application, but it's different: Currying is a pattern of de-composing an N-arity function into a chained series of N 1-arity functions.

Let's get practical

While JS is not a inherently Functional language, using a number of functional patterns and concepts can make it more declarative and result in clearer, more useful code.

FP Example


var jQuery = (function ($, Math) {
  $.fn.allHeights = function() { // look, no loops!
    return this.map( function() { // create new aray from jQ objs
      return $(this).outerHeight(); // new array is of heights
    }).get(); // get just the keyed values and no JQ props
  };

  $.fn.equalHeight = function () {
    var heights = $(this).allHeights(); // call our jQ plugin
    // "borrow" the max method and apply our array as args
    return $(this).css('height',  Math.max.apply(null, heights););
  }; //                                          ^ this
  return $; // module pattern
}(jQuery, Math));

$.equalHeights('#my-selector');