Score:2

How do I properly define a library so that JS will execute after a certain condition is met?

cn flag

I'm creating a custom module to execute some JS AFTER the cache is cleared. Here is the structure of the module:

Module -> flush.info.yml

name: Flush
type: module
description: A very important module.
package: Custom
version: 1.0
core_version_requirement: ^8 || ^9

Module -> flush.libraries.yml

flush:
  version: 1.x
  js:
    js/flush.js: {}
  dependencies:
    - core/jquery
    - core/drupalSettings

Module -> flush.module

<?php

use Drupal\Core\Form\FormStateInterface;

/**
 * Implementation of hook_cache_flush()
 */

  function flush_cache_flush() {
    \Drupal::state()->set('flush_cache_cleared', TRUE);
  }

  function flush_page_attachments(array &$attachments) {
    if (\Drupal::state()->get('flush_cache_cleared')) {
      $attachments['#attached']['library'][] = 'flush/flush';
     \Drupal::state()->set('flush_cache_cleared', FALSE);
    }
  }

Module -> JS -> flush.js

(function ($, Drupal, drupalSettings) {
    'use strict';

    Drupal.behaviors.flush = {
      /**
       * Drupal attach behavior.
       */
      attach: function (context, settings) {
        this.settings = this.getSettings(settings);
        alert("flush!");
        console.log ("Hello World");
      },
    };
  })(jQuery, Drupal, drupalSettings);

Both functions in flush.module work properly, however the JS that is #attached which is simply supposed to use an alert() never runs. I believe the issue is related to this line specifically : $attachments['#attached']['library'][] = 'flush/flush';

Is there something obvious I'm doing wrong? I used page_attachments as the method to attach the JS because it does not necessarily have to be associated with a specific part of the page, or a render array, etc. Please let me know if you have any potential ideas as to what the issue could be, thanks!

sonfd avatar
in flag
What happens if you run it without the if statement in hook_page_attachments?
Joseph avatar
cn flag
@sonfd The JS still doesn't work. I know the if statement triggers as intended because if I echo the JS alert() command inside of it, it triggers. It's only when I try to attach the JS via the library that nothing happens.
Jaypan avatar
de flag
Did you clear the cache after adding that code?
Joseph avatar
cn flag
@Jaypan Yeah, clearing the cache works and if I add JS into the function flush_page_attachments() it will work as intended (i.e. an alert when the cache is cleared) however using a library which is the correct way of doing it doesn't work.
fr flag
It is perfectly valid to attach a library inside hook_page_attachments() like you are trying to do - I have working code that does that. I don't see anything wrong with your hook_page_attachments(). BUT if you have added or changed flush.libraries.yml after you enabled your flush module, then the new libraries file might not be picked up. Try uninstalling and re-installing your module.
Jaypan avatar
de flag
That was my point.
Joseph avatar
cn flag
Clearing the cache doesn't make my library work, nor does uninstalling and reinstalling the custom module. When I add JS to hook_page_attachments() (like an alert) it seems to pop up after the cache clears but before the page reloads. Could this be why attached JS via the library isn't appearing? I feel like it should still trigger an alert like it does when it's added directly into hook_page_attachments(). Not sure though.
apaderno avatar
us flag
Are you sure you are editing the correct file? Did you check the module isn't copied in two different directories?
apaderno avatar
us flag
Attaching JavaScript code as library is what Drupal core does. It even uses the same hook you are using. If there is anything wrong, that is not the used hook. See [`contextual_page_attachments()`](https://api.drupal.org/api/drupal/core%21modules%21contextual%21contextual.module/function/contextual_page_attachments/9.3.x) as example of what Drupal core does. IMO, if the correct way to add JavaScript code were adding a block, Drupal core would do that.
apaderno avatar
us flag
See also the content of [contextual.libraries.yml](https://api.drupal.org/api/drupal/core%21modules%21contextual%21contextual.libraries.yml/9.3.x), from which it's clear that the *drupal.contextual-links* library contains JavaScript code.
sonfd avatar
in flag
I think you just need a more complete definition of what “after the cache was cleared” means. Is that an absolute truth for the universe at the moment the cache was cleared? Does a user need to be on the site before the cache was cleared for it to be significant? Is it a period of time that lasts X seconds after a cache clear?
sonfd avatar
in flag
The way you are trying to use state will not work. Your js will only be attached for literally a single page request from a single person in the universe after a cache clear.
apaderno avatar
us flag
@sonfd The code can be changed to add the library to the first X page requests done after the cache is cleared. I don't understand why it has be added after the cache is cleared, though. Probably, seeing which JavaScript code is attached and why would help in giving a better answer.
Score:2
cn flag

I wouldn't use Drupal::state() or hook_page_attachments(), those are too static. Better use a session value and a block, see my comment in the previous question.

Set the session value in the hook when the cache is cleared and only if not running from the command line:

function flush_cache_flush() {
  if (PHP_SAPI !== 'cli') {
    \Drupal::request()->getSession()->set('run_flush_js', TRUE);
  }
}

In the block check the session value and remove it so that it doesn't persist after the redirect.

src/Plugin/Block/FlushBlock.php

<?php

namespace Drupal\flush\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a flush block.
 *
 * @Block(
 *   id = "flush_flush",
 *   admin_label = @Translation("Flush"),
 *   category = @Translation("Custom")
 * )
 */
class FlushBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    $session = \Drupal::request()->getSession();  
    $build = [];
    if ($session->get('run_flush_js')) {
      $session->remove('run_flush_js');
      $build['content'] = [
        '#markup' => '<div class="run-flush-js"></div>',
        '#attached' => ['library' => ['flush/flush']],
      ];
    }
    $build['#cache']['max-age'] = 0;
    return $build;
  }
}

js/flush.js

(function ($, Drupal, once) {
  'use strict';

  Drupal.behaviors.flush = {
    /**
     * Drupal attach behavior.
     */
    attach: function (context, settings) {
      if (once('flush', '.run-flush-js', context).length) {
        alert("flush!");
      }
    }
  };
})(jQuery, Drupal, once);

flush.libraries.yml

flush:
  js:
    js/flush.js: {}
  dependencies:
    - core/drupal
    - core/jquery
    - core/once

Edit: Update js code, see How can I make this JavaScript code get executed when the BigPipe module is enabled?

apaderno avatar
us flag
`hook_page_attachments()` is used to conditionally add attachments to a page before it's rendered, and the state API is used to store site state values. The OP isn't doing anything wrong in using them. The suggested alternative has its cons: A session value is per user and a block needs to be added to a theme region (which also means that changing theme could cause the library not to be added).
apaderno avatar
us flag
Then, using a block and a session value won't resolve the issue the OP is noticing. It could be the OP edited the .libraries.yml file after installing the module, or the OP is editing the wrong .libraries.yml file (which could also happen because there are two copies of the same module); in both the cases, changing the way the library is added to the page doesn't fix the real issue.
4uk4 avatar
cn flag
I think you need a session, without you run the javascript on a random client, whoever is requesting the first page after the cache is cleared. There are other ways than a block, but it's probably the quickest way because you can use the system how Drupal renders pages, This works even with BipPipe. I've tested it with and without and you can see how this effects the page loading.
apaderno avatar
us flag
The OP didn't say he wanted to run the JavaScript code for the same user who caused the cache to be cleared; if that were the case, though, using a session value isn't necessary, as the user who caused the cache to be cleared would be visiting at least two different pages or the same page twice. Even supposing the OP wanted to be sure to add the JavaScript code to the page requested by the user who caused the cache to be cleared (which doesn't make sense, since the cache is cleared for every user), it would be sufficient to store the user ID using the state API.
apaderno avatar
us flag
That's make sense because the user who was logged in when the cache was cleared is a state value for the site, not a session value.
Score:1
in flag

I think you should approach this differently. Here's what I would do:

First, implement hook_cache_flush() and use it to store the last cache flush time to Drupal state.

function flush_cache_flush() {
  \Drupal::state()->set('flush_cache_cleared', time());
}

Next, use hook_page_attachments() to always attach your javascript on every page, but pass the the cache cleared timestamp to your javascript.

function flush_page_attachments(array &$attachments) {
  $attachments['#attached']['library'][] = 'flush/flush';
  $attachments['#attached']['drupalSettings']['flush_cache_cleared'] = \Drupal::state()->get('flush_cache_cleared');
}

Last, in your javascript, check if the new cache cleared time is more recent than the previous (you'll need to store this on the client side via local storage or something).

If the times are different, you know the cache was cleared.

(function ($, Drupal, drupalSettings) {
  'use strict';

  Drupal.behaviors.flush = {
    attach: function (context, settings) {

      let cacheFlushedTime = drupalSettings['flush_cache_cleared'];
      // Do stuff with the cache_flushed_time here, like
      // compare it to a previously known cache flushed time...

    },
  };
})(jQuery, Drupal, drupalSettings);
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.