Problem/Motivation

People often need to add JS and CSS to a Drupal site. In Drupal 7, we had drupal_add_js() and drupal_add_css(), which was pretty easy to use. In Drupal 8, those have been removed, and it's not really clear to people what to use as a replacement.

Use cases:

a) Module developer needs to add JS/CSS files to all pages.

b) Module developer needs to add JS/CSS files to just some pages (when a certain block is visible, contingent on some PHP code, or a page they module creates).

c) Module developer needs to add dynamic JS/CSS snippets to all or some pages, generated in PHP code.

d) Themer/site builder (not a strong programmer, and ideally working in the theme) needs to add JS/CSS files to all pages.

e) Themer/site builder needs to add JS/CSS files to just some pages, possibly contingent on some PHP code.

f) Themer/site builder has a JS/CSS snippet that they found on the web or that was given to them or that they found (for example, with a font they purchased, etc.). They need to add it to the HTML HEAD of all pages.

g) Themer/site builder has a JS/CSS snippet (as in f). They need to add it to the HTML HEAD of just one or a few pages, or maybe the snippet has a variable in it that they need to read from theme settings.

Note: The module developer use cases (a), (b), and (c) are NOT part of this issue. See #2301851: Make sure module developers have info on adding js/css to pages.

It is not currently obvious to themers at least, how to do any of these.

What themers are instructed to do on this change record: https://www.drupal.org/node/2228783

1. Define a library using a themename.libraries.yml file. (instructions on this change record: https://www.drupal.org/node/2201089)
2. Make that library go on every page by adding it to the libraries section of the theme.info.yml file.

This only seems to cover the cases where the CSS/JS to be added is in files, and they need to go on every page (case (d) only).

For other cases, the solutions (including hooks) that you might think of also have problems:

- If it's just static JS/CSS and goes on all pages, then you can just add it to the html.html.twig file, but that only applies to case (f).

- hook_preprocess_html() seems to be too late in the process to use #attached to solve case (f).

- hook_preprocess_page() suggested for case (f) in http://drupal.stackexchange.com/questions/95635/adding-js-to-a-drupal-8-... resulted in two copies of the script being added to the page (see several comments in this issue). The trick turned out to be to make sure 'every_page' is set to TRUE (see comment #27). Here's some sample code:

function atomic_page_alter(&$page) {
  if (theme_get_setting('atomic_typekit_id')) {
  	$kit_id = theme_get_setting('atomic_typekit_id');
  	$kit_url = "//use.typekit.net/" . $kit_id . ".js";
  	$kit_try_catch = "try{Typekit.load();}catch(e){}";
	$page['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url, 'every_page' => TRUE);
	$page['#attached']['js'][] = array('type' => 'inline', 'data' => $kit_try_catch, 'every_page' => TRUE);
  }
}

- hook_page_build() is only for modules.

- This works, but ... ugh! drupal_render()? 
<code>
/**
 * Implements hook_preprocess_page()
 */
function atomic_preprocess_page(&$vars) {
  if (theme_get_setting('atomic_typekit_id')) {
    $kit_id = theme_get_setting('atomic_typekit_id');
    $kit_url = "//use.typekit.net/" . $kit_id . ".js";
    $kit_try_catch = "try{Typekit.load();}catch(e){}";
    $kit = array();
    $kit['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url);
    $kit['#attached']['js'][] = array('type' => 'inline', 'data' => $kit_try_catch);
    drupal_render($kit);
  }
}

Proposed resolution

Given the 4 themer-related use cases (are there any more?):

1. What is the procedure the person should use, and is it straightforward?

2. Is it currently documented? Ideally, if you go to the D8 landing page for api.drupal.org, there should be a topic that either explains how to do this, or links to a documentation page (not a change notice) on drupal.org in the theming guide with all the details.

3. There should also be a change notice explaining the differences between how you would have done this in Drupal 7 and how you should now do this in Drupal 8.

Remaining tasks

1. Figure out the procedure for each use case. If there isn't one that is viable/straightforward, make an API for doing it.

2. Make sure documentation and change notices exist.

The documentation for theme adding JS/CSS should go on: https://www.drupal.org/node/2216195 (or nearby)

And this information should be linked from one of the landing pages linked to on the api.drupal.org home page for Drupal 8 (we might need to add a new landing page, combined possibly with #2301851: Make sure module developers have info on adding js/css to pages).

User interface changes

None.

API changes

TBD.

CommentFileSizeAuthor
#18 alertify.js_.jpg125.9 KBmarkhalliwell

Comments

rainbowarray’s picture

Issue summary: View changes
webchick’s picture

Priority: Normal » Major
Related issues: +#1996238: Replace hook_library_info() by *.libraries.yml file

Thanks, Marc. I raised this as a concern back when {#1996238] was in review (and in various patches to remove drupal_add_js() calls), but the front-enders who replied there seemed to be on board. Also pinging that issue to see if we can get some cross-traffic back over here.

To me, I'd still like to see something like a drupal_add_js() equivalent that worked well with the render cache. *Most* JS (at least at the theme level) is not complicated with numerous dependencies. It's either a quick bit of inline JS for one particular page, or a quick call-out to an external library as you've done here. And it really sucks that you need to learn Drupalisms instead of just being able to follow the creator's instructions for commonly used libs such as type kit. :( I'm guessing the only reason you didn't become more stuck is because you were aware of the drupal_add_js()/libraries.yml issues to begin with. :(

webchick’s picture

joelpittet’s picture

This DX worries me a bunch! The only solution that came to mind was adding drupal_add_js/css() back in, just for the time being doing drupal_render() inside it until the refactor of the Asset/Render API is a reality.

Though stop me if someone has a better idea, I'm all ears and eyes on this one. Just trying to help the DX regression during the transition to something better...

Assetic needs help, it's quite a complicated beast from what I've seen so far and may be why others haven't jumped onboard to help sboyer finish it. I've seen it has near 100% code coverage from what I saw in Austin from Sam's awesome work on it! Weighting seems to be quite a different thing that needs to be tackled for the differences between Drupal now and the new pattern(which I think is part of Assetic), and eventually exposing (if not already done) the Assetic filters to contrib. That's the cursory stuff I know of it...

And last note, as a themer, if this isn't easy... I'm just going to hard code the assets into the html template and save the hour+ of looking this shit up! And I have done that a bunch in D7, *hands washing motion* cache busting averted... especially with the example given in the IS, typekit will likely be on every page.

markhalliwell’s picture

Priority: Major » Normal

From my experience with TypeKit, it is often best to put the scripts it provides directly in the html.html.twig template of the theme itself. More specially, when dealing with advanced aggregation/manipulation, there can be undesired consequences in where exactly the elements are produced (which can lead to fonts "flickering", which really has more to do with TypeKit's JS implementation). TypeKit works "best" if it's the first thing loaded (IMO).

I'm really not convinced this is a "libraries.yml" issue, or anything closely related for that matter. Preprocess the page should allow you to do:

  $variables['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url);
  $variables['#attached']['js'][] = array('type' => 'inline', 'data' => $kit_try_catch);

Or possibly (cannot recall off the top of my head):

  $variables['page']['content']['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url);
  $variables['page']['content']['#attached']['js'][] = array('type' => 'inline', 'data' => $kit_try_catch);

Because preprocessing is basically altering existing render arrays (it is ultimately calling drupal_render() too), its just a matter of figuring out where to attach something in the array. The other alternative is to intercept a different theme hook (one that isn't "page", which is really special to begin with).

Sadly this has more to do with the render "API" or lack there of IMO. I'm very tempted to mark as "works as designed" since we have known this is really the issue, most of which has been postponed for 9.x IIRC. I will however I'll settle with reducing priority so adequate documentation can be created instead.

markhalliwell’s picture

Issue tags: +theme system cleanup
markhalliwell’s picture

Issue tags: +Render API
joelpittet’s picture

Priority: Normal » Major

re #5 You can't do the first example, though that would be nice...

The second example will only work if content is rendered to the page twig template via {{ content }}. Which is most cases granted, it's not an absolute. And when you say "'every_page' => TRUE"! you don't mean every page that renders {{ content }}.

We shouldn't really pick on TypeKit example in the issue summary because that is quite irrelevant to the DX issue. Thought yeah, put it in the template;)

For the first example to work we'd have to likely treat 'variables' and 'render element(s)' the same I guess... and put it in _theme() after the preprocess functions are invoked. Right after:

  if (isset($info['preprocess functions'])) {
    foreach ($info['preprocess functions'] as $preprocessor_function) {
      if (function_exists($preprocessor_function)) {
        $preprocessor_function($variables, $hook, $info);
      }
    }
  }

Duplicated from drupal_render

  // Add additional libraries, CSS, JavaScript and other custom
  // attached data associated with this element.
  if (!empty($variables['#attached'])) {
    drupal_process_attached($variables);
  }

Though I think this may have some hardships due to render cache because drupal_process_attached() get's called before _theme() and inside of the render_cache. @see _drupal_render_process_post_render_cache().

Going to agree with @webchick here, this is major.

nod_’s picture

Few things: I agree that typekit example is an edge case and would be best served by a module so it can do all sort of things for users. Having a url depend on a configurable variable is not that common. There are two main problems left for me with the DX:

  1. Adding settings to drupalSettings.
  2. The current concept of inline JS.

I don't know about the drupal_render() thing very much. It's been a while since we removed drupal_add_js and most of it's uses and the idea was that using drupal_render was a stopgap to facilitate #1762204: Introduce Assetic compatibility layer for core's internal handling of assets which would replace that stuff. That said, some details:

Settings

are a real issue. They can't ever be in the libraries yml file. The previous way to do that was drupal_add_js(array(), 'settings');, right now it looks like this:

$element['#attached']['js'][] = array(
  'data' => array('someSetting' => 'yay!'),
  'type' => 'settings',
);

Not really friendly.

almost like we'd want a $element['#attached']['settings'] array to help with this. It was discussed at some point, maybe jesse or wim have a better memory of where we ended up.

Inline JS

A tricky problem. inline JS lives in PHP. I've worked on websites where inline js was messing everything up, it was not fun to fix. Having JS live in PHP means you can violate coding standards all you want, eslint will never pick it up and could crash everything.

What I'd propose is to add a new type, file-inline or inline-file that would pick the content of the file and display it as inline JS. We can eslint the code, we can pipe it through a minifier or whatever. And we can use libraries to declare it and let's be honest, if your inline JS has no dependencies I'm not sure what you're using inline JS for…

inlined-js:
  js:
    js/file-to-inline.js: {type: inline-file}
  dependencies:
    - core/drupalSettings
    - core/jquery

as for the implementation I hear you saying no way we'd do a file_get_content on every request. In my nav timing module I inline a file, I take the content and stuff that in a variable that gets refresh on cache clear. It works and the inline js is pretty small. Now it'd be cool if core had a built in way of doing this (for CSS too) and use the cache properly and all. Having that option means it's easier to do some perf improvments on some sites where all it's JS could be inlined.

Doing things this way means that drupalSettings is then required to pass variables from PHP to the inline script. Which is fine but means we'd need to make the settings DX nicer to use.

So my position on inline JS is we need to remove the option altogether and print the content of files inlined. In the typekit example it'd be

typekit:
  js:
    js/typekit-load.js: {type: inline-file}
  dependencies:
    - core/drupalSettings

and because there is a variable in the url from typekit, use hook_library_alter() (This hook is only invoked once per library and page.):

sometheme_library_alter(&$library, $name) {
  if ($name == 'sometheme/typekit') {
    // add the typekit file and drupalSettings values.
  }
}
moshe weitzman’s picture

I think hook_page_alter() is still a good place for this. The OP said "That resulted in two copies of the script being added to the site, and for the life of me I couldn't figure out why.". I think this merits some research. If we fix that (if needed), we have a good solution here.

rainbowarray’s picture

Yes, I would have been fine with hook_page_alter if it hadn't resulted in two copies of the script.

Just to be clear, the thing that probably would have been the biggest help was better documentation on how to do this.

One of the methods markcarver mentioned in #5 was:

$variables['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url);

Confession: I tried something like that in both preprocess_html and preprocess_page before figuring out that no, $variables isn't a render element, so that wouldn't do anything. It might be foolish that I didn't realize that right away, but it's something I tried.

I guess, yes, this could be done with a custom html.html.twig with a preprocess function passing the kit ID in as a variable. I try not to hard code things like that into templates, but yes, that's an option.

As for creating a file that contains inline JS, then pulling that in with libraries.yml. I guess? Frankly I'm unfamiliar with drupalSettings, but as long as it's well-documented, I'm up for trying whatever. What I'm still unclear about though is if libraries.yml can handle external scripts on other servers. In this particular case it's the external script that needed the file name replaced with a theme settings variable: the inline script didn't have any variables.

The external script use case is important to understand how to handle, as it's not uncommon to reference a script on a CDN, such as a Google hosted library.

Maybe assetic would take care of these use cases. I've looked a bit at assetic, but I'm not sure how it handles inline or external scripts.

If assetic is what would best handle this, then maybe the answer for now is to improve documentation and focus on getting assetic working with core.

Didn't mean to cause so much hubbub. Just ran into some thorny challenges getting this working.

nod_’s picture

Turns out hook_page_build() should be used instead of hook_page_alter(). I had forgotten. See quickedit_page_build().

The code sample for page build add CSS to the page, so that's the one to use instead of hook_page_alter and preprocess.

markhalliwell’s picture

Title: Clear up DX of adding inline JS (and external JS?) through a theme » Add documentation on how to add inline/external JS
Component: theme system » documentation
Priority: Major » Normal
Issue tags: -theme system cleanup, -Render API

Ah, yes. I've used hook_page_build() in 7.x only a couple of times as we have the drupal_add_*() functions readily available (and would work before) in alter/preprocess. With the removal of those functions in 8.x and how "unique/complex" the page theme hook can be, this does make more sense. So this is really just an issue about proper documentation. Marking appropriately.

rainbowarray’s picture

So in theory it looks like hook_page_build() should definitely do the trick.

In practice... it did not.

Here's what I tried:

/**
 * Implements hook_preprocess_page()
 */
function atomic_page_build(&$page) {

  //////////////////////////////
  // Add in TypeKit Code.
  //////////////////////////////
  if (theme_get_setting('atomic_typekit_id')) {
  	$kit_id = theme_get_setting('atomic_typekit_id');
  	$kit_url = "//use.typekit.net/" . $kit_id . ".js";
  	$kit_try_catch = "try{Typekit.load();}catch(e){}";
	$page['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url, 'every_page' => TRUE);
	$page['#attached']['js'][] = array('type' => 'inline', 'data' => $kit_try_catch, 'every_page' => TRUE);
  }
}

Cleared cache, made sure the theme setting was correct for the kid ID... and no scripts were added to the source code. Tried clearing cache multiple times. I had tried it without the every_page => TRUE first, then added that in based on one of the examples. Neither way worked.

Sorry.

lewisnyman’s picture

Issue tags: -Front end +frontend
jhodgdon’s picture

This issue please needs an issue summary telling what needs to be documented, since it's now a documentation issue? The issue summary is too vague and implies we need a code change, as do the first few comments (and then I didn't have time to read the rest).

benjy’s picture

Well if #14 is correct then this isn't just documentation since we now have a regression.

markhalliwell’s picture

StatusFileSize
new125.9 KB

The reason #14 doesn't work is because hook_page_build() is a module hook. Themes cannot use it.

I've done a little digging to see what exactly works and what doesn't (any my best understanding of why):
https://gist.github.com/markcarver/fbf19bbfa83ca1134da7

Here is a screenshot of the above external and inline resources working in a "stark2" theme:

I don't exactly have time to update the issue summary at the moment. I can, however, verify that this is indeed just a documentation issue.

rainbowarray’s picture

It looks like hook_page_alter is the best option when not using drupal_render in preprocess_page. When I tried hook_page_alter, for some reason it ran twice, and the scripts were added twice. No idea why. I can try it again tonight to see if I get different results.

webchick’s picture

Another person trying and struggling to do this https://twitter.com/emmajanehw/status/486530525859827712

I really think this is more than a documentation issue. I feel that the APIs are simply not working the way that themers expect.

jhodgdon’s picture

So...

According to #18, it looks like a theme can implement hook_page_alter() in their theme file and add any JS/CSS they need to the render array, if they need logic to decide when to add which file(s) -- although mdrummand seems to be saying that two copies of the scripts were added to the page, and as noted above, we need to investigate this. We have unfortunately lousy documentation about #attached and render arrays in general -- see #1617948: [policy for now] New standard for documenting form/render elements and properties...

Also, as noted in #4/#5 and other comments, if there are script tags or CSS or whatever that need to be on every page, they should just be able to add them to the page template file (page.html.twig or html.html.twig).

So... what are the problems with this? To me it seems fairly straightforward if it works... What do we need to document and where, or what do we need to change in the code?

rainbowarray’s picture

The only documentation I found on how to do this was on Stack Exchange. I guess if somebody googles how to do this, they might find this issue now. Still, it seems like there should probably be something on d.o. that explains how to add inline/external js (and possibly css).

Right now it's unclear if this is just a documentation issue, as I wasn't able to get the recommended method of using hook_page_alter to work as expected.

I'm also unclear on why hook_page_build only works for modules. Particularly if there are examples showing how to add in JS with hook_page_build, it seems strange that we'd tell modules to do things one way and themes another.

Crell’s picture

I am certain to get tomatoes thrown at me, but there relying on hook_page_build and hook_page_alter is a bad idea. The page manager/SCOTCH work that Tim and EclipseGc are working on is likely to remove one or both of those. The structure of the array is unreliable, and the move is very clearly toward domain objects in that part of the system. (HtmlPage, etc.) You can add things to HtmlPage via a view listener already. The question is whether HtmlFragment/HtmlPage currently support inline/external JS. They may not; I've not tried. If not, we should fix that.

That said, libraries is, AFAIK, the "correct" way to add CSS/JS these days, for better or worse. If it doesn't support inline/external JS, let's fix that. Hacking around it with the old "arrays of doom" technique is a losing battle and robs us of an opportunity to make the new architecture more robust.

webchick’s picture

I do definitely agree that docs on D.o should exist for this (most likely a topic for CSS/JS under "User interface" at https://api.drupal.org/api/drupal/8 that gives an overview, linking to a sub-page in https://www.drupal.org/developing/api/8 with specific examples of inline vs. external vs. settings vs...).

However, I think that if our APIs are confusing enough that you must read documentation in order to do what is a 90%+ use case in themes, we have failed in our goals to make D8 more approachable for front-end devs.

jhodgdon’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

So let's step back a moment and think about the DX in general... Maybe we should think through the use cases a bit? Here's the list I came up with:

a) Module developer needs to add JS/CSS files to all pages.

b) Module developer needs to add JS/CSS files to just some pages (when a certain block is visible, contingent on some PHP code, or a page they module creates).

c) Module developer needs to add dynamic JS/CSS snippets to all or some pages, generated in PHP code.

d) Themer/site builder (not a strong programmer, and ideally working in the theme) needs to add JS/CSS files to all pages.

e) Themer/site builder needs to add JS/CSS files to just some pages, possibly contingent on some PHP code.

f) Themer/site builder has a JS/CSS snippet that they found on the web or that was given to them or that they found (for example, with a font they purchased, etc.). They need to add it to the HTML HEAD of all pages.

g) Themer/site builder has a JS/CSS snippet (as in f). They need to add it to the HTML HEAD of just one or a few pages, or maybe the snippet has a variable in it that they need to read from theme settings.

So. Given these 7 use cases (are there any more?):

1. What is the procedure the person should use, and is it straightforward?

2. Is it currently documented? Ideally, if you go to the D8 landing page for api.drupal.org, there should be a topic that either explains how to do this, or links to a documentation page (not a change notice) on drupal.org (in the developer or themer guide) with all the details.

3. There should also be a change notice explaining the differences between how you would have done this in Drupal 7 and how you should now do this in Drupal 8.

I'm going to go ahead and add this to the issue summary, and update it with the other suggestions/thoughts here.

sun’s picture

Issue tags: +API change, +beta target

Solutions for some of the stated use-cases and possible DX improvements will require backwards-incompatible API changes, so tagging accordingly.

Case a) to c) are about modules, which should be removed from this discussion. Modules are able to manage/load asset libraries in sophisticated ways in D8 already. If there's any issue with that → separate issue.

For all other cases, let's bear in mind that themes are operating at the very end of the entire rendering chain. Let's not try to invent APIs for things that nothing interfaces with.

To expand on that:

  1. .libraries.yml registers libraries for later usage in PHP code. All use-cases that do not involve custom/conditional PHP code in a theme do not and should not use .libraries.yml.

  2. Instead, the theme's .info.yml allows to load files ad-hoc, while still making included assets visible and available for (module + base theme) manipulation to hook_js|css_alter().

  3. If no PHP/preprocessing is supposed to be involved at all (e.g., site-wide, non-aggregated, possibly CDN-hosted assets), then a theme can and should load asset files directly from within Twig template files via raw HTML. Twig templates support (simple) conditional logic structures, if necessary.

Additionally, note that all of the above options are cached in different ways; changes are not visible until respective caches are cleared:

(1) requires to purge the asset library discovery cache[s], (2) requires to purge a very sad mess of system_list() + ThemeHandler caches, (3) requires to purge precompiled Twig templates [unless twig_debug/twig_auto_reload is enabled in settings.php].


Lastly, in response to this Twitter thread:

#1996238: Replace hook_library_info() by *.libraries.yml file introduced explicit support for protocol-relative URLs in .libraries.yml. The stylesheet-* properties in theme .info.yml files are processed by different code. To my knowledge, that code does not support protocol-relative URLs (yet).

rainbowarray’s picture

Good news! I got this working thanks to markcarver's gist. Here's the code that ultimately worked for me:

/**
 * Implements hook_page_alter()
 */
function atomic_page_alter(&$page) {

  //////////////////////////////
  // Add in TypeKit Code.
  //////////////////////////////
  if (theme_get_setting('atomic_typekit_id')) {
  	$kit_id = theme_get_setting('atomic_typekit_id');
  	$kit_url = "//use.typekit.net/" . $kit_id . ".js";
  	$kit_try_catch = "try{Typekit.load();}catch(e){}";
	$page['#attached']['js'][] = array('type' => 'external', 'data' => $kit_url, 'every_page' => TRUE);
	$page['#attached']['js'][] = array('type' => 'inline', 'data' => $kit_try_catch, 'every_page' => TRUE);
  }
}

I would guess the key thing that made this work is the 'every_page' => TRUE bit. My wild speculation is that when I was looking at the home page, maybe hook_page_alter was firing more than once, and stating that the scripts should appear on every page somehow shortcircuited that? That may be wildly wrong, but I'd suggest that if there is some documentation on this, the 'every_page' => TRUE part should be made clear.

Putting this directly in the template would have been another option too. To me, it feels somewhat weird to put JS scripts directly into a template. But that would definitely work.

jhodgdon’s picture

Issue summary: View changes

mdrummond pointed out in IRC where JS docs for themers should go, so adding link to the issue summary: https://www.drupal.org/node/2216195

Plus updating with working code from previous mdrummond comment.

I also filed
#2301851: Make sure module developers have info on adding js/css to pages
to deal with the related issue for module developers, since this issue should stay focused on themers.

rainbowarray’s picture

So just a note that the solution that worked, hook_page_alter, might get deprecated per Crell in #23. I very much don't understand the HtmlPage fragment thing that he mentioned as an alternative, but I should be seeing him at a talk Monday night, so maybe he can explain that to me a little more then. :)

danny englander’s picture

Re #23: possible removal of hook_page_build and hook_page_alter, is there an issue open for that. Thanks!

Crell’s picture

There's no dedicated issue for it. Rather, it would be a side effect of refactoring that's been on-again, off-again in the rendering pipeline. EclipseGc or Tim Plunkett could say what the current status is better, I think.

webchick’s picture

Spun off #2316783: Need new topic on adding CSS/JS about an API.d.o topic page for this. We still don't have an answer to the question of what to do instead of hook_page_alter() though, it seems.

webchick’s picture

webchick’s picture

Title: Add documentation on how to add inline/external JS » Add documentation on how to add inline/external JS for themers

Oops. I missed that Jennifer had already done that, so #2301851: Make sure module developers have info on adding js/css to pages it is.

Clarifying title. FTR, I still find it offensive as hell this gets poo-pooed as a docs issue, but whatever.

sun’s picture

Title: Add documentation on how to add inline/external JS for themers » Fix DX/TX of adding/adjusting inline/external JS in themes
Component: documentation » asset library system
Issue summary: View changes
Issue tags: +TX (Themer Experience)

I still find it offensive as hell this gets poo-pooed as a docs issue

My last comment in #26 basically tried to state the same, so let's fix that.

(striking out a) to c) in the issue summary, since separate issue.)

rainbowarray’s picture

For the record, I solved this in my particular case was to write the JS into the html.html.twig template, then send the Typekit ID variable used with the script through preprocess_html. But I only really did that because I wanted the advanced Typekit embed to appear as one of the first things in the head element, rather than lower down where the rest of the JS typically shows up. That's an issue specific to Typekit embedding.

So... I'm not sure. If page_alter and page_build are out, then I guess you could maybe attach JS or CSS with [#attached] to a render element in preprocess_page? That's not terribly intuitive, though. And you'd really only need that if there was something dynamic about the JS or CSS you were attaching.

The real key is to make sure that external JS (and to a lesser extent CSS), as well as inline JS and possibly CSS, can be added via libraries.yml.

My understanding from talking to Crell is that HTML Fragment Objects might be useful for this as well. To get that working correctly, we need to get Assetic compatibility working correctly, and there's an issue for that here: #1762204: Introduce Assetic compatibility layer for core's internal handling of assets. I'll see if I can take a look at that during TC Drupal.

jhodgdon’s picture

Issue summary: View changes

Found some docs on how to accomplish one of the tasks, updating summary.

catch’s picture

hook_page_build() and hook_page_alter() are still fine for adding css/js that otherwise can't be in libraries.yml at the moment, system module does this.

If they get removed, then there'll need to be an alternative mechanism for themers to add css/js that's compatible with the new method. Since the replacement method does not exist yet, it's not helpful telling people they shouldn't use the current one either. I think that's the cause of least part of the confusion here.

@nod_ those are good points about how bad inline js is at the moment. I hardly ever see it used, but when I do it's usually been mis-used where a js file + Drupal.settings would have been fine (and then it breaks aggregation because the aggregate the js would otherwise have been included in, gets split into one before and one after). The inline-from-file approach is worth a new issue as an API addition in itself.

catch’s picture

Status: Active » Postponed (maintainer needs more info)

Is there anything remaining here that's not handled by #2346369: Support special '#attached' variable for attaching assets in preprocess functions now? That issue was committed so I think we're OK.

jhodgdon’s picture

Status: Postponed (maintainer needs more info) » Active

Um... The use cases are set out in the issue summary. What is a **themer** supposed to do to accomplish each use case? We need to figure out what that is, and make sure there is documentation.

The issue summary seems clear enough... what are themers supposed to do, for each use case, and where is it documented? If it's not possible to accomplish, or not documented somewhere that you can get to from the api.d.o d8 landing page and the Theming Guide by following intuitive links, then this issue needs to stay open. If it's all possible to accomplish and documented, then let's close this. But I haven't seen anyone list out what a themer is supposed to do yet for the 4 use cases in the summary, aside from case (d) which seems to be covered by some YML files (or at least it was a while back).

catch’s picture

d) and e) are both covered by #2346369: Support special '#attached' variable for attaching assets in preprocess functions which is why I linked to it.

The answer to F and G should usually be 'stick it in a file then use d or e'.

jhodgdon’s picture

Ok then... It sounds like we need to make sure all of this information is documented for themers, both in the Theme guide on https://www.drupal.org/theme-guide/8/adding-javascript and in something linked to from the api.d.o landing page for D8.

Let's see... I've updated that page, and I think it now covers the use cases for adding static JS/CSS to all pages or a subset of pages. And I see that the information on how to attach libraries is covered in brief on https://api.drupal.org/api/drupal/core!modules!system!theme.api.php/grou... too so that is good... oh not so good, it says:

... unconditional page-level assets (loaded on all pages where the theme is in use): these are defined in the theme's *.info.yml file.

so this is wrong and needs to be fixed. I think there should also be an explanation of how to define a library or a link to that information.

But there is still some information that I don't see covered... and don't know how to do. How would you handle the case where the JS to be added is computed on the fly in PHP? Is that even possible to do in a theme (or for that matter in a module)? I added a TODO section to the theme guide page... what can we put in there?

And can someone review https://www.drupal.org/theme-guide/8/adding-javascript and make sure it is accurate?

wim leers’s picture

How would you handle the case where the JS to be added is computed on the fly in PHP?

JS as in inline JS or as in JS settings?

jhodgdon’s picture

RE #43, are those two separate cases? If so, then answer both? I really don't know what all people need to do in their custom and contrib themes, but the need to put in dynamic JS that is computed in the PHP somehow was brought up in various comments, and is not currently addressed on the docs page.

Actually, there's an example in the issue summary: needing to get some information from the database about a font kit and load a particular JS library accordingly.

See also comments #5, 9, 11, 26, 27, ... well there are a lot of comments about this above.

wim leers’s picture

The answer is that you should never generate inline JS. You should avoid inline JS like the plague. Instead, you should create a proper JS file that uses drupalSettings (i.e. JS settings) to know what it should do — e.g. needing to get some information from the database about a font kit.

I've updated https://www.drupal.org/theme-guide/8/adding-javascript with that information (and fixed formatting). I also reviewed it, and it was accurate already :)

catch’s picture

Title: Fix DX/TX of adding/adjusting inline/external JS in themes » Documentation updates adding/adjusting inline/external JS in themes
Component: asset library system » documentation

Re-titling since there's no code changes here with $variables['#attached'] landing.

https://www.drupal.org/theme-guide/8/adding-javascript looks good to me.

webchick’s picture

Cmd+Fing https://www.drupal.org/theme-guide/8/adding-javascript for "inline" doesn't return anything. Considering this is a general HTML thing, and considering there are ad networks, etc. that instruct developers to insert their JS inline, we should probably cover this case. Even if it's just "copy the inline JS into a file called foo.js and attach it like this."

catch’s picture

One issue that could change things here is #2368797: Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks, prevent CSS/JS aggregation from taking down sites and use HTTP GET for AJAX requests - which is going to either recommend or enforce using libraries everywhere rather than individual files.

wim leers’s picture

#47: added a brief section about inline JavaScript.

webchick’s picture

Ok, that covers the very basics, but let's start taking some actual use cases here.

https://support.google.com/adsense/answer/181960?ref_topic=28896&rd=1

How do I get that into my Drupal site?

jhodgdon’s picture

@Wim Leers: THANK YOU, that was the missing piece! Very nice and clear (the section on Settings I mean). I learned something. :)

So, next steps:

a) Let's make sure this is covered for themers in something they'd find on api.d.o, or at least linked from an api.d.o landing topic. The basics of library attachment are covered in the Render API docs:
https://api.drupal.org/api/drupal/core!modules!system!theme.api.php/grou...
However:

1. Themers probably wouldn't know they need to read this topic -- the only relevant-looking topic from the D8 landing page would be the Theme topic:
https://api.drupal.org/api/drupal/core!modules!system!theme.api.php/grou...

2. There's no information in the Render API topic on how to actually define a library or attach a library. It actually says to use JS and CSS directly. Ugh.

3. It would be helpful to have a link to this nice page we've been working on.

===> So let's add a section to the Theme topic (need a patch!) with a section title like "Adding JavaScript and CSS to pages" and ... well we should probably just link to the Render API topic rather than covering this again?
===> Let's also update the Render API section so it tells how to define a library, links to https://www.drupal.org/theme-guide/8/adding-javascript, and it should also probably link to this other page, which is aimed at module developers: https://www.drupal.org/node/2274843

b) I agree with #50 that we need a section on https://www.drupal.org/theme-guide/8/adding-javascript with an actual use case where the JS you find on the web is split up properly into a static file and some settings? We could call this section "Adapting scripts you find online" or something similar.

c) On #2301851: Make sure module developers have info on adding js/css to pages we also need to make sure that similar information is available to module developers, once (a) and (b) are taken care of. That will mean updating https://www.drupal.org/node/2274843 so it looks more like https://www.drupal.org/theme-guide/8/adding-javascript but aimed at module developers.

wim leers’s picture

#50: that's a good example of a common case, but a bad example of "inline JS".

You see, inline JS (as in D7 drupal_add_js('JS code here', array('type' => 'inline'))) just gets executed. That's it.

The example you cite is different, in that it is going to essentially replace itself with actual HTML, to render an ad. Hence that's not the inline JS you would attach via drupal_add_js(), it's the type of JS you want to insert as markup. Typically in a block, or in a template.


#51

I'm very glad to hear you're so satisfied :)

  • a) Sounds good :)
  • b) OK. Can you give another example of inline JS that isn't actually "markup JS"?
  • c) Makes sense!
jhodgdon’s picture

Hm. Well maybe we should add a note about the JS that is actually "markup JS" and leave it at that? Just for completeness?

wim leers’s picture

jhodgdon’s picture

Great!

OK, so I think that all that remains on this issue is to make sure someone going to api.drupal.org can find this wonderful page. This is "next step (a)" in comment #51 above.

rainbowarray’s picture

External scripts: Could we have some clarification on how to handle external CDN scripts? Is it possible to reference those in libraries.yml? It's possible those would have dependencies like other JS scripts, so simply hard-coding them in a Twig file seems counter-intuitive. If they are sitewide, then yes, hard-coding in Twig would work.

Inline scripting: It's also entirely possibly that inline JS may have dependencies as well, where it should only be placed on certain pages. There are definitely valid use cases for inline JS either in the head section or at the bottom of the HTML, because inline JS (and CSS) avoids server hits, each of which results in latency, particularly on mobile devices. Again, if this inline JS is sitewide or in a block, then we have this covered.

The documentation does help me better understand how libraries work, although I'm somewhat concerned now that I better understand the implications.

The end result of this is that our performance optimization is based on trying to only load the relevant CSS/JS for a particular page, thus reducing the file size of any aggregated files. However, this also greatly reduces the ability for browsers to cache these assets as somebody navigates throughout the site. Rather than loading the JS and CSS up front when first visiting the site, and then using cached versions of those files as somebody goes from page to page, instead the browser will need to load new JS/CSS aggregate files on pages with different requirements. Overall, that means both larger total file sizes being downloaded and an increase in the number of times the browser hits the server, taking a latency penalty each time. If page A has a 12k script aggregate file due its dependencies and page B has a 14k script aggregate file due to its dependencies, but if dependencies were ignored there would have been a 20k script aggregate file, then somebody is downloading 26k of scripts vs 20k, which isn't a savings.

In an HTTP 2 world, without asset aggregation, this presumably matters less.

Anyhow. That ship has sailed.

If somebody wants to attach all their CSS and JS sitewide rather than using dependencies, they can. Flexibility is good.

However, I'm still not clear that we have solid workflows for dependency-based external and inline scripts. Ironically, inline scripts actually could benefit the most from being able to have dependencies, since they're hard-coded into HTML and thus wouldn't be cached globally anyhow. To the main point, could we clarify these two use cases?

jhodgdon’s picture

RE #56, those are good questions!

For externally-hosted JS, the documentation page https://www.drupal.org/node/2274843 that is linked to from the libraries section of https://www.drupal.org/theme-guide/8/adding-javascript says that you can use a URL instead of a local file path. So I added a clarifying note to the theme guide page section that indicates to go there to get details on how to use an external URL, adding CSS, and "other possibilities". Since they're part of Libraries, it does seem like they support dependencies. So I think this part is taken care of?

For Inline JS, the idea would be to put the inline JS into a block, and put that block where the particular inline JS needs to be located (header, footer, sidebar, etc.), and only on the desired pages, right? That is what the inline JS section of https://www.drupal.org/theme-guide/8/adding-javascript suggests. Then if this inline JS has dependencies, the way you'd ensure they're met is to make sure the libraries it depends on are put into the right pages.

I don't see a problem with this... What more do you think we need to document?

rainbowarray’s picture

Good to hear external scripts work!

A block can't put inline JS into the head HTML element, and there problem isn't a region to place a block in the same location as other footer JS (although I suppose you could create one), so I don't think that solves it. What would be nice is if you could store inline JS in a file, put the file reference in libraries.yml, but then add an option to render the contents of that file inline (in head or footer), rather than aggregating the contents or linking to the file if not aggregating.

jhodgdon’s picture

Um... I guess I'm not understanding this completely.

If you want to put JavaScript into the HTML head tag... the right way to do that seems to be to write the JS you need into a file, make it a library, and use #attached to attach it in an appropriate THEME_preprocess_HOOK() implementation. Or if it's dynamic, use the "settings" method described in the section about "Attaching configurable JavaScript". Right?

For the HTML footer... Can you give an example of this, so we can figure out the right way to do it? I thought most of the time if you think you should put JS in the HTML "footer" (which doesn't really exist, right? It's just meaning "end of the body")... I thought the right way to do that kind of thing would be to put it inside a JQuery document.load method?

markhalliwell’s picture

A block can't put inline JS into the head HTML element

Why can't you just override html.html.twig and put this inline script there?

edit: I mean to say, isn't this one of the reasons why page.tpl.php was split into html.tpl.php in 7.x.

jhodgdon’s picture

@Mark Carver - I think the question was "on only certain pages".

nod_’s picture

Talking with Wim about all this I'm pushing for getting rid of inline JS as we know it, so that we can't have JS in YML files or in PHP. Js will have to be in a .js file or in a twig template somewhere. It means we need to add functionality to keep that "feature" possible (and with the ability to handle dependencies). That's what #58 mention and what's described in #9.

Also note that what I talked about in the first part of that comment is RTBC and doc are updated #2382557: Change JS settings into a separate asset type. Meaning the DX issue preventing us from going totally that way is no longer a problem. Let's agree that is what we want and open an issue about it.

jhodgdon’s picture

@nod_ -- I'm a bit confused aobut #62....

I definitely like the issue summary's proposed resolution in #2382557: Change JS settings into a separate asset type -- that looks good.

But what are you meaning about getting rid of or changing in addition to that?

wim leers’s picture

#63: the second point in #9:

The current concept of inline JS.

wim leers’s picture

Status: Active » Fixed

AFAICT everything is now fully documented in https://www.drupal.org/node/2216195. The module developer docs have also been updated: https://www.drupal.org/node/2274843.

jhodgdon’s picture

GREAT WORK WIM!!!!!! Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

pfrenssen’s picture

Status: Closed (fixed) » Needs work

It is still not clearly documented how to add an external library. The documentation at https://www.drupal.org/node/2216195 says this:

You can also have the JS come from an external URL, include CSS files, and there are other possibilities. See https://www.drupal.org/node/2274843 or https://www.drupal.org/node/2201089 for details.

That is not helpful for this common use case. What's worse is that I'm sent on a rabbit chase. When I look at https://www.drupal.org/node/2274843 it says this:

You can also have the JS come from an external URL, include CSS files, and there are other possibilities. See https://www.drupal.org/node/2201089 for details.

When I finally end up at https://www.drupal.org/node/2201089 there is not a single example of using an external library or CDN, instead it hints that external URLs and stream wrappers are supported, however if I try this:

angularjs:
  version: 1.4.4
  js:
    https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js: {}

Then it doesn't work, the URL is stored as a relative path to my theme, this is how it looks in the debugger:

[
  'group' => -100,
  'type' => 'file',
  'data' => 'themes/mytheme/https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js',
  'version' => '1.4.4',
  'minified' => false,
]
pfrenssen’s picture

I found an example in the tests, from common_test.libraries.yml:

external:
  version: 1
  js:
    http://example.com/script.js: { type: external }
  css:
    theme:
      http://example.com/stylesheet.css: { type: external }

So the missing piece seems to be to specify the type as external. Wouldn't be bad to document these options too.

pfrenssen’s picture

Status: Needs work » Fixed

Added an example of a javascript library hosted on an external CDN to https://www.drupal.org/node/2274843, https://www.drupal.org/node/2216195 and https://www.drupal.org/node/2201089. I chose AngularJS as an example. I also removed the "rabbit-hole" chain of links between these pages, instead I referred to the new information lower on the page.

moshe weitzman’s picture

Thanks pfrenssen.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.