Problem/Motivation

The AddCssCommand (typically used by AjaxResponseAttachmentsProcessor) always adds the missing-but-required CSS assets to the beginning of the HTML <head>.

Theme developers often rely on the loading order of the CSS assets, but due to the functionality described above, this becomes impossible when an ajax-driven operation happens on the page.

Proposed resolution

After superficially checking the CssCollectionRenderer, I would say it works fine: It returns the required CSS assets in the expected order (respecting the library dependencies).

Since we know that we have all off the required dependencies in the 'before-ajax' state, and the newly added CSS assets seems to be in the expected order, my proposed solutions are:

  1. Modify ajax.js's add_css command to add the CSS assets after the last stylesheet referrer link on the page <link rel="stylesheet">.
  2. Append to the HTML <head> (or <body> ?).

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Original report

See ajax_render()

And more specifically these lines:

  if (!empty($styles)) {
    $extra_commands[] = ajax_command_prepend('head', $styles);
  }

When you try to add stylesheets through ajax, isn't the expected behaviour that these stylesheets actually do something?
By prepending them to the head, you basically give them the lowest possible priority towards all other already loaded stylesheets.

Suggestion: use ajax_command_append() instead.

Example (shoddy, but it illustrates the problem):

  • I want to add and trigger a jQuery dialog through AJAX
  • I load the files using drupal_add_library and drupal_add_css in my AJAX callback
  • The JavaScript behaves as expected, the CSS doesn't because it has to yield to the system stylesheet for dialogs
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

nod_’s picture

Version: 7.12 » 8.x-dev

That's an issue indeed.

JeremyFrench’s picture

This is interesting.

My initial thought is that you would add CSS to style new content which you are adding via ajax which may not be in the original request. As these would (hopefully) have different classes/selectors to the content in the page it doesn't matter if you are prepending or appending to head. I believe that order precedence in CSS is only important if all other precedence are equal.

So your example (with jquery ui) should still work prepending or appending. Unless you are using IE then you will be caught by #1071818: Lazy-loading CSS fails in IE

The questionable advantage of appending is that you would be able to change existing styles.

One solution would be to provide an option which defaults to prepend, but allows you to append if you really want to.

effulgentsia’s picture

Doing append() would mean module CSS overriding theme CSS, right? Is it possible to merge into the correct place? We already have <style> and <link> tags organized by library vs module vs theme, but I don't know how we can expose that information to the tags themselves for JS to insert each one in the right place.

JeremyFrench’s picture

but I don't know how we can expose that information to the tags themselves for JS to insert each one in the right place.

In theory it may be possible to change the text of the CSS, or the CSS in the DOM, but looking at this compatibility table I think we would be walking into a cross browser nightmare.

nod_’s picture

another good use-case for data- maybe?

kristiaanvandeneynde’s picture

@JeremyFrench I agree this needs to be validated so it doesn't break other use cases.

As suggested in #2 and #3: having the option to choose where you want the style to be placed would be an even better solution than my suggestion in the original post.

But keep in mind that if you are adding CSS through an event-triggered AJAX call (so not lazy loading after page load), it is very likely that you want that CSS to do something.

In the dialog example you have two possible workflows:

  • Module adds a dialog, but doesn't want to specifically style it
    1. Stylesheets get loaded on page load, or lazy loaded right after
    2. Theme (or system) stylesheet applies
  • Module wants to add a dialog and style it (for instance hide the 'close me' button or add an icon)
    1. Stylesheet gets loaded on page load, or lazy loaded right after
    2. Module stylesheet gets loaded and prepended to all other stylesheets
    3. Theme (or system) stylesheet applies instead of module's stylesheet
JeremyFrench’s picture

@kristiaanvandeneynde

Stylesheet gets loaded on page load, or lazy loaded right after
Module stylesheet gets loaded and prepended to all other stylesheets
Theme (or system) stylesheet applies instead of module's stylesheet

This is why you would namespace your attributes/styles. That way it doesn't matter if they are prepended or appended.

If you don't do this and you append styles you will end up with CSS clashes and changing the styles on the page.

edit adding link to Specifity guide, remember position only affects what rules apply if specificity is equal, and then rules are combined rather than replaced.

In short if your module/library needs position to have it's styles apply it is probably not using CSS correctly. The only case when this is not true is if you are intentionally trying to change the styles already on a page.

kristiaanvandeneynde’s picture

I realize forcing a high specificity (or, as you call it, namespacing your css) is a possible solution, but is it not the be all end all solution.

Suppose a very naughty themer added !important to one of his stylesheets. No matter how much I would namespace my CSS or how much I would add !important myself; if my stylesheet were loaded before his, I'd still be screwed out of theming my newly added HTML.

Search for "specificity wars" for more information on that subject.

Specificity wars are what happens when developers start trying to beat each others specificity, rather than having a real solid architecture and code standards. Eventually, like the worst offender in this case with 526 important properties, you can end up in a case where nearly every property is marked important.

Source: http://www.stubbornella.org/content/2010/07/01/top-5-mistakes-of-massive-css/

kristiaanvandeneynde’s picture

Priority: Normal » Minor
Issue tags: +DrupalWTF

Setting to minor since this would only cause problems in <1% of use cases and an easy workaround is namespacing.

Doesn't mean this shouldn't be further examined, though.

ezman’s picture

After a lot of time spent trying various methods, I've come up with the following workaround which may help others.

I have a custom module that provides AJAXified editing capabilities using Dialog API.
My problems were twofold:
1) jQuery UI's CSS was being loaded into the Dialog after my theme's CSS, and I had customised the look of the Dialog in my theme's CSS.
2) Media's media.js kept loading in after my theme's script.js, and I had overriden Drupal.behaviors.mediaElement in my theme's Javascript.

I tried various methods of altering the AJAX commands, and also tried setting $form['#attached']['js] and ['css'] properties, but nothing worked.
Here's what's worked for me. It's not pretty, but it seems to do the trick. Even after managing to get my CSS output at the end, I found that the media.js file was still being loaded after my theme's, so used the jQuery update hook_js_alter() method to swap Media's media.js for my own version.

function MYMODULE_ajax_render_alter(&$commands) {
	$csspath = drupal_get_path('theme', 'MYTHEME') . '/style.css';
	$jspath = drupal_get_path('theme', 'MYTHEME') . '/script.js';
	//first unset the existing ones, and then add them onto the end 
	//(inside a command array the path has initial / prepended)
	if ( isset( $commands[0]['options'] ) ) {
		unset( $commands[0]['options']['js'][array_search( '/'.$jspath, $commands[0]['options']['js'] )] );
		unset( $commands[0]['options']['css'][array_search( '/'.$csspath, $commands[0]['options']['css'] )] );
		$commands[0]['options']['css'][] = '/'.$csspath; //ajax_command_append('head',$css);
		$commands[0]['options']['js'][] = '/'.$jspath; //ajax_command_append('head',$js);
	}
	$css = drupal_add_css( $csspath, array('group' => CSS_THEME, 'weight' => 20, 'every_page' => false) );
	$js = drupal_add_js( $jspath, array('group' => JS_THEME, 'scope' => 'header', 'weight' => 20, 'every_page' => false, 'defer' => true) );
	$commands[] = ajax_command_append('head',$css);
	$commands[] = ajax_command_append('head',$js);
}
function MYMODULE_js_alter(&$javascript) {
//swap default media.js for our one
	$javascript['sites/all/modules/media/js/media.js']['data'] = drupal_get_path('module', 'MYMODULE') . 'media.js';
}

You'll notice I've added the JS and CSS twice, and I don't know which of the two methods is the one that works, or if both are necessary (It's very late, been working the whole weekend, finally got it working now, and wanted to note this down before I forget it and move on to the next major problem tomorrow morning. will probably have to revisit this and tidy it up a little bit later on)
But I hope this helps someone and stops them spending as long as I have done trying to get it working!

David_Rothstein’s picture

Title: ajax_render() prepending stylesheets beats the purpose? » ajax_render() always prepends new stylesheets to <head>, regardless of the 'group' and 'weight' settings
Issue summary: View changes
Priority: Minor » Normal
Issue tags: +Needs backport to D7

Setting to minor since this would only cause problems in <1% of use cases and an easy workaround is namespacing.

I don't think this is an edge case - the CSS system allows 'group' and 'weight' to be specified on CSS files for a reason, and it's bad that they are sometimes ignored. Giving your CSS rules a higher specificity can help, but it's a workaround and not always a good option (for example, if the two files that are competing with each other come from code you don't control, e.g. an outside library, a contrib module, etc).

In the meantime here's the workaround I wound up using for this, if it helps anyone (this is Drupal 7 code):

/**
 * Implements hook_ajax_render_alter().
 */
function MYMODULE_ajax_render_alter(&$commands) {
  $libraries_added = drupal_static('drupal_add_library');
  if (!empty($libraries_added['MYMODULE']['MYLIBRARY'])) {
    foreach ($commands as &$command) {
      if ($command['command'] == 'add_css') {
        $command = ajax_command_append('head', $command['data']);
      }
    }
  }
}

(where MYLIBRARY is a library defined in MYMODULE's hook_library() that has the CSS files which I want to force to be appended)

This is simpler code than the example in the previous comment, but also more brute-force (it will force all CSS files added via that Ajax request to be appended, not just my own). If you care about the difference, then the earlier code example may work better for you.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

huzooka’s picture

Title: ajax_render() always prepends new stylesheets to <head>, regardless of the 'group' and 'weight' settings » Fix AJAX add_css – insert the needed CSS assets after the already-inserted ones
Version: 8.6.x-dev » 8.9.x-dev
Issue summary: View changes
huzooka’s picture

Issue summary: View changes
seanB’s picture

Just ran into this when we enabling bigpipe for an existing site (found this while investigating #2886657: BigPipe changes CSS evaluation order). I personally think appending is a better default than prepending. Here is a simple patch to change this for anyone who needs it.

This is just a quickfix though. I agree the "real" solution would be to insert all CSS as you should expect based on the styling level, weight and dependencies.

Wim Leers’s picture

@seanB Any reason to not mark this Needs review and have a test run?

seanB’s picture

Status: Active » Needs review

Not really, let's see.

joegraduate’s picture

The attached patch is a re-roll of #20 that should apply cleanly to 8.9.x.

#20 applies cleanly to 8.8.x but not 8.9.x.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

joegraduate’s picture

The attached patch is a 9.1.x re-roll of #23.

joegraduate’s picture

Issue tags: +Global2020

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

joegraduate’s picture

KapilV’s picture

Status: Needs review » Needs work

The last submitted patch, 29: 1461322-29.patch, failed testing. View results

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

seanB’s picture

Just found something funky because of this patch.

  1. The file /assets/vendor/jquery.ui/themes/base/core.css is added via multiple CSS libraries in core (core/drupal.dialog, core/drupal.autocomplete , core/jquery.ui).
  2. When an AJAX request is executed, Drupal adds new CSS libraries to the page based on drupalSettings.ajaxPageState. This only compares libraries, not actual CSS/JS files.
  3. This could lead to /assets/vendor/jquery.ui/themes/base/core.css being added a second time, and having .ui-front { z-index: 100; } override .ui-dialog { z-index: 1260; } from /themes/seven/css/components/dialog.css, since the new CSS files are now appended instead of prepended.

Fixed it with some custom CSS in our theme for now. Since jQuery UI is being removed it is probably not worth adding a new issue to make sure core/drupal.dialog, core/drupal.autocomplete , core/jquery.ui don't add the same file, even though that is the real cause.

Just spent some time figuring this out, so thought it would be good to share :).

huzooka’s picture

@seanB, I think that #32 should be addressed as well.

And we definitely need test coverage.

nickvanboven’s picture

#32 also seeing this when using the gin theme and this patch, css of media library is added after gin css and breaks the media library display.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

trackleft2’s picture

My theory is that we have at least three ways of interacting with libraries.

Overrides
Dependencies
Extensions

This patch is great for overrides and dependencies, but extensions are a problem. See https://www.drupal.org/node/2497313

Overrides need to replace a library
Dependencies need to prepend a library
Extensions need to be appended to a library

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

joegraduate’s picture

Re-rolled patch from #25 for 10.0.x.

joegraduate’s picture

Re-rolled patch from #40 for 10.1.x.

acbramley’s picture

Bit confused by this one, I've spent the day debugging some odd CSS issues with paragraphs and entity embed modals.

I've tracked it down to /core/themes/claro/css/components/jquery.ui/theme.css being added multiple times on the page when adding a new paragraph (added via the add_css command) but these CSS files already are appearing after the existing CSS files. This leads to some UI styling bugs like the one described here.

EDIT: Ahhh, prepend is only used if response.data is a string, which is now deprecated. So it's using loadjs instead.