2

I'm trying to send a variable to my JavaScript. I would like to send a class name to my twig template and the same class-name to my javascript from a function that returns a render array. The function is called from a routing yml to display a page from a link in a page (which is beside the point).

The returned render array is similar to the following one.

return [
  '#theme' => 'theme_name',
  '#form' => $form,
  '#data' => [
    'customer' => $obj_customer,
    'customer_subscription' => $obj_customer->getSubscription(),
    'subscriptions_explanation' => $arr_subscription_text,
  ]
];

Passing data from PHP to javascript variable available to all pages in Drupal 8 says you can do something with '#attached' => ['drupalSettings' => ['do' => $something]] in hook_page_attachments(), but that doesn't seem to apply to the method I am using.

How can I do in my case? Adding stylesheets (CSS) and JavaScript (JS) to a Drupal theme doesn't seem to cover this use case.

In my JavaScript I define my JS functions inside:

(function ($, Drupal, drupalSettings) {
  Drupal.behaviors.portalCore = {
    someFunction() {
      ...
    }
  };
});

Is there perhaps a way to attach a variable to the portalCore from my example code?

EDIT (edited):

After applying kiamlaluno's answer: So If I call the drupalSettings in the console I see my assigned variable in there. But if I try to reach that variable from within (function ($, Drupal, drupalSettings) {} I get a cannot read property of undefined, obviously as logging drupalSettings logs undefined. When I remove the drupalSettings from (function ($, Drupal, drupalSettings) {}, I can reach my variable through drupalSettigs.myVar.

I, obvjously, should have included the drupalSettings in the final parentheses of the closure, like so (thank you @kiamlaluno):

(function ($, Drupal, drupalSettings) {
  Drupal.behaviors.portalCore = {
    attach(context, settings) {
      ...
    },
    functionName() {
      // Do stuff
    },
  };
})(jQuery, Drupal, drupalSettings);
  • You had it right with ['#attached']['drupalSettings'], perhaps if you change the question to concentrate on your current implementation of that? – Clive Jul 14 at 9:37
  • 1
    Ask a separate question for the javascript issue. The original question is quite useful and should stay on-topic, because it is important to add metadata to render arrays and use the mentioned hook only in rare, exceptional cases. – 4k4 Jul 14 at 12:30
  • @4k4 I could ask a seperate question but then could answer my own question... – Sl4rtib4rtf4st Jul 14 at 13:02
3

As described in Render API overview (the Attaching libraries in render arrays section), #attached can be always used in a render array. If your function/method returns a render array, it could return the following render array.

return [
  '#attached' => [
    'drupalSettings' => ['do' => $something],
  ],
  '#theme' => 'theme_name',
  '#form' => $form,
  '#data' => [
    'customer' => $obj_customer,
    'customer_subscription' => $obj_customer->getSubscription(),
    'subscriptions_explanation' => $arr_subscription_text,
  ]
];

(It doesn't matter where is the #attached array added. You could also add it to the end of the render array.)

Note that, to avoid conflicts with other modules, it would preferable to prefix the setting name (do, in your case) with the module machine name (for example, as in mymodule_do).

As for using drupalSettings in a Drupal behavior, see the content of core/misc/machine-name.es6.js, for example.

/**
 * @file
 * Machine name functionality.
 */

(function($, Drupal, drupalSettings) {
  /**
   * Attach the machine-readable name form element behavior.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches machine-name behaviors.
   */
  Drupal.behaviors.machineName = {
    /**
     * Attaches the behavior.
     *
     * @param {Element} context
     *   The context for attaching the behavior.
     * @param {object} settings
     *   Settings object.
     * @param {object} settings.machineName
     *   A list of elements to process, keyed by the HTML ID of the form
     *   element containing the human-readable value. Each element is an object
     *   defining the following properties:
     *   - target: The HTML ID of the machine name form element.
     *   - suffix: The HTML ID of a container to show the machine name preview
     *     in (usually a field suffix after the human-readable name
     *     form element).
     *   - label: The label to show for the machine name preview.
     *   - replace_pattern: A regular expression (without modifiers) matching
     *     disallowed characters in the machine name; e.g., '[^a-z0-9]+'.
     *   - replace: A character to replace disallowed characters with; e.g.,
     *     '_' or '-'.
     *   - standalone: Whether the preview should stay in its own element
     *     rather than the suffix of the source element.
     *   - field_prefix: The #field_prefix of the form element.
     *   - field_suffix: The #field_suffix of the form element.
     */
    attach(context, settings) {
      const self = this;
      const $context = $(context);
      let timeout = null;
      let xhr = null;

      function clickEditHandler(e) {
        const data = e.data;
        data.$wrapper.removeClass('visually-hidden');
        data.$target.trigger('focus');
        data.$suffix.hide();
        data.$source.off('.machineName');
      }

      function machineNameHandler(e) {
        const data = e.data;
        const options = data.options;
        const baseValue = $(e.target).val();

        const rx = new RegExp(options.replace_pattern, 'g');
        const expected = baseValue
          .toLowerCase()
          .replace(rx, options.replace)
          .substr(0, options.maxlength);

        // Abort the last pending request because the label has changed and it
        // is no longer valid.
        if (xhr && xhr.readystate !== 4) {
          xhr.abort();
          xhr = null;
        }

        // Wait 300 milliseconds for Ajax request since the last event to update
        // the machine name i.e., after the user has stopped typing.
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        if (baseValue.toLowerCase() !== expected) {
          timeout = setTimeout(() => {
            xhr = self.transliterate(baseValue, options).done(machine => {
              self.showMachineName(machine.substr(0, options.maxlength), data);
            });
          }, 300);
        } else {
          self.showMachineName(expected, data);
        }
      }

      Object.keys(settings.machineName).forEach(sourceId => {
        const options = settings.machineName[sourceId];

        const $source = $context
          .find(sourceId)
          .addClass('machine-name-source')
          .once('machine-name');
        const $target = $context
          .find(options.target)
          .addClass('machine-name-target');
        const $suffix = $context.find(options.suffix);
        const $wrapper = $target.closest('.js-form-item');
        // All elements have to exist.
        if (
          !$source.length ||
          !$target.length ||
          !$suffix.length ||
          !$wrapper.length
        ) {
          return;
        }
        // Skip processing upon a form validation error on the machine name.
        if ($target.hasClass('error')) {
          return;
        }
        // Figure out the maximum length for the machine name.
        options.maxlength = $target.attr('maxlength');
        // Hide the form item container of the machine name form element.
        $wrapper.addClass('visually-hidden');
        // Initial machine name from the target field default value.
        const machine = $target.val();
        // Append the machine name preview to the source field.
        const $preview = $(
          `<span class="machine-name-value">${
            options.field_prefix
          }${Drupal.checkPlain(machine)}${options.field_suffix}</span>`,
        );
        $suffix.empty();
        if (options.label) {
          $suffix.append(
            `<span class="machine-name-label">${options.label}: </span>`,
          );
        }
        $suffix.append($preview);

        // If the machine name cannot be edited, stop further processing.
        if ($target.is(':disabled')) {
          return;
        }

        const eventData = {
          $source,
          $target,
          $suffix,
          $wrapper,
          $preview,
          options,
        };

        // If no initial value, determine machine name based on the
        // human-readable form element value.
        if (machine === '' && $source.val() !== '') {
          self.transliterate($source.val(), options).done(machineName => {
            self.showMachineName(
              machineName.substr(0, options.maxlength),
              eventData,
            );
          });
        }

        // If it is editable, append an edit link.
        const $link = $(
          `<span class="admin-link"><button type="button" class="link">${Drupal.t(
            'Edit',
          )}</button></span>`,
        ).on('click', eventData, clickEditHandler);
        $suffix.append($link);

        // Preview the machine name in realtime when the human-readable name
        // changes, but only if there is no machine name yet; i.e., only upon
        // initial creation, not when editing.
        if ($target.val() === '') {
          $source
            .on('formUpdated.machineName', eventData, machineNameHandler)
            // Initialize machine name preview.
            .trigger('formUpdated.machineName');
        }

        // Add a listener for an invalid event on the machine name input
        // to show its container and focus it.
        $target.on('invalid', eventData, clickEditHandler);
      });
    },

    showMachineName(machine, data) {
      const settings = data.options;
      // Set the machine name to the transliterated value.
      if (machine !== '') {
        if (machine !== settings.replace) {
          data.$target.val(machine);
          data.$preview.html(
            settings.field_prefix +
              Drupal.checkPlain(machine) +
              settings.field_suffix,
          );
        }
        data.$suffix.show();
      } else {
        data.$suffix.hide();
        data.$target.val(machine);
        data.$preview.empty();
      }
    },

    /**
     * Transliterate a human-readable name to a machine name.
     *
     * @param {string} source
     *   A string to transliterate.
     * @param {object} settings
     *   The machine name settings for the corresponding field.
     * @param {string} settings.replace_pattern
     *   A regular expression (without modifiers) matching disallowed characters
     *   in the machine name; e.g., '[^a-z0-9]+'.
     * @param {string} settings.replace_token
     *   A token to validate the regular expression.
     * @param {string} settings.replace
     *   A character to replace disallowed characters with; e.g., '_' or '-'.
     * @param {number} settings.maxlength
     *   The maximum length of the machine name.
     *
     * @return {jQuery}
     *   The transliterated source string.
     */
    transliterate(source, settings) {
      return $.get(Drupal.url('machine_name/transliterate'), {
        text: source,
        langcode: drupalSettings.langcode,
        replace_pattern: settings.replace_pattern,
        replace_token: settings.replace_token,
        replace: settings.replace,
        lowercase: true,
      });
    },
  };
})(jQuery, Drupal, drupalSettings);

Note the first line and the last line of the closure.

(function($, Drupal, drupalSettings) {
  // ...
})(jQuery, Drupal, drupalSettings);

Note also that the behavior's attach() method receives drupalSettings as second parameter. In fact, the method that attaches the behaviors uses the following code. (settings = settings || drupalSettings;)

Drupal.attachBehaviors = function(context, settings) {
  context = context || document;
  settings = settings || drupalSettings;
  const behaviors = Drupal.behaviors;
  // Execute all of them.
  Object.keys(behaviors || {}).forEach(i => {
    if (typeof behaviors[i].attach === 'function') {
      // Don't stop the execution of behaviors in case of an error.
      try {
        behaviors[i].attach(context, settings);
      } catch (e) {
        Drupal.throwError(e);
      }
    }
  });
};

As side note, if your JavaScript library depends from drupalSettings, it must declare its dependency from it, as in the following library definition. (The definition has been taken from user.libraries.yml.)

drupal.user.permissions:
  version: VERSION
  js:
    user.permissions.js: {}
  dependencies:
    - core/jquery
    - core/jquery.once
    - core/drupal
    - core/drupalSettings
    - user/drupal.user.admin
| improve this answer | |
  • Thanks for the effort (and the edits), how do I then access these settings? If I do (function ($, Drupal, drupalSettings) {}. A console.log of drupalSettings logs undefined, and the Drupal object doesn't seem to contain the variable... – Sl4rtib4rtf4st Jul 14 at 9:50
  • 1
    In the sample code in your question, you haven't passed the global drupalSettings into your closure, so you would expect it to be undefined (you also haven't passed jQuery or Drupal FWIW) – Clive Jul 14 at 9:59
  • 1
    @Sl4rtib4rtf4st It's lunch time here. I will expand the answer when I return from lunch. – kiamlaluno Jul 14 at 10:14
  • To add to what @Clive said (I already added it to the answer), if a JavaScript library (which is the way to add JavaScript code to pages) depends from drupalSettings, it must declare its dependency from it. – kiamlaluno Jul 14 at 13:48
  • @kiamlaluno Are you supposed to define your custome functions in the attach(){} function? – Sl4rtib4rtf4st Jul 15 at 7:42
5

It does apply to the method you are using. You can add metadata like #cache or #attached to any render element (#theme, #type, #markup, #plain_text):

return [
  '#theme' => 'theme_name',
  '#form' => $form,
  '#data' => [
    'customer' => $obj_customer,
    'customer_subscription' => $obj_customer->getSubscription(),
    'subscriptions_explanation' => $arr_subscription_text,
  ],
  '#attached' => ['drupalSettings' => ['do' => $something]],
];
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.