Problem/Motivation

When adding messages in javascript applications such as the Media Library it would be nice to be able to rely on a core ajax command, rather than having to recreate all of this functionality for each application.

See #3059955-22: It is possible to overflow the number of items allowed in Media Library

Proposed resolution

Quoting @phenaproxima:

We should add a new AJAX command, \Drupal\Core\Ajax\AddMessageCommand, which can add messages to a Drupal.Message instance associated with a particular selector. It should also have an option to clear existing messages.

So, the JavaScript side:

// In JavaScript, always do this unconditionally.
$(someElement).prepend('<div class="target-selector"></div>');

Drupal.AjaxCommands.prototype.addMessage = function (response) {
  const messages = new Drupal.Message(document.querySelector(response.selector));
  messages.add(response.message, response.id, response.type);
}

And on the PHP side:

use Drupal\Core\Ajax\AddMessageCommand;

return (new AjaxResponse())
  ->addCommand(new AddMessageCommand('.target-selector', 'You chose too many things!', 'limit exceeded', 'error')->clearAll());

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Comments

oknate created an issue. See original summary.

oknate’s picture

oknate’s picture

Issue summary: View changes
oknate’s picture

oknate’s picture

I make a change in #3059955-34: It is possible to overflow the number of items allowed in Media Library to allow clearing the previous messages. I think we should either have that feature or add a separate command for it.

wim leers’s picture

Issue tags: +JavaScript
wim leers’s picture

Category: Feature request » Task
Priority: Normal » Major
Issue tags: +blocker
oknate’s picture

Status: Active » Needs review
StatusFileSize
new9.77 KB

Here's @bnjmnm's patch for #3081526: Add announcement after clicking 'Save and select' in the media library. with the code specific to that issue removed.

droplet’s picture

Why not make it more generic?

`Drupal.Message` is Class. You can add another parameter to call the method rather than specific `add` only

e.g.:

message(ajax, response) {
  const {method, args} = response
  Drupal.Message[method].apply(args); // OR Drupal.Message[method](...args);
}
oknate’s picture

StatusFileSize
new288.8 KB
new7.62 KB
new13.06 KB

Here's a duplicate message bug + fix.

Re #10, that's an interesting idea. I'll explore that later tonight.

Note, I added this change because it would throw an error when calling clear when using the default selector, this is probably a separate bug:

diff --git a/core/misc/message.es6.js b/core/misc/message.es6.js
index 252d629310..cc6fb62806 100644
--- a/core/misc/message.es6.js
+++ b/core/misc/message.es6.js
@@ -162,6 +162,9 @@
      * @name Drupal.Message~messageDefinition.clear
      */
     clear() {
+      if (!this.messageWrapper) {
+        this.messageWrapper = Drupal.Message.defaultWrapper();
+      }
       Array.prototype.forEach.call(
         this.messageWrapper.querySelectorAll('[data-drupal-message-id]'),
         message => {

Status: Needs review » Needs work

The last submitted patch, 11: 3086096-11.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

oknate’s picture

Status: Needs work » Needs review
StatusFileSize
new1.56 KB
new13.09 KB

I forgot to commit changes to the test form.

wim leers’s picture

Component: ajax system » javascript
Issue tags: +Needs subsystem maintainer review
+++ b/core/lib/Drupal/Core/Ajax/MessageCommand.php
@@ -0,0 +1,85 @@
+   * @param string|null $messageWrapperQuerySelector
+   *   The query selector of the element to display messages in when they
+   *   should be displayed somewhere other than the default
+   *   [data-drupal-messages].
...
+    $this->clearPrevious = $clearPrevious;

+++ b/core/misc/ajax.es6.js
@@ -1562,5 +1566,29 @@
+      if (response.clearPrevious) {
+        messages.clear();
+      }

+++ b/core/misc/message.es6.js
@@ -162,6 +162,9 @@
     clear() {
+      if (!this.messageWrapper) {
+        this.messageWrapper = Drupal.Message.defaultWrapper();
+      }

I think these aspects need to be reviewed & approved by the people who led #77245: Provide a common API for displaying JavaScript messages.

EDIT: Pinged @nod_ and @tedbow.

droplet’s picture

tedbow’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Ajax/MessageCommand.php
    @@ -0,0 +1,85 @@
    +   * @var boolean
    +   */
    +  protected $clearPrevious;
    

    I think we are suppose to use @var bool instead

  2. +++ b/core/lib/Drupal/Core/Ajax/MessageCommand.php
    @@ -0,0 +1,85 @@
    +   *   The query selector of the element to display messages in when they
    +   *   should be displayed somewhere other than the default
    +   *   [data-drupal-messages].
    

    Could we remove data-drupal-messages and put a @see or mention of the JS method Drupal.Message.defaultWrapper(). Because it is little more complicated.

  3. +++ b/core/misc/ajax.es6.js
    @@ -655,7 +655,9 @@
    -        `An error occurred while attempting to process ${this.options.url}: ${e.message}`,
    +        `An error occurred while attempting to process ${this.options.url}: ${
    +          e.message
    +        }`,
    
    @@ -748,7 +750,9 @@
    -        `An error occurred while attempting to process ${ajax.options.url}: ${e.message}`,
    +        `An error occurred while attempting to process ${ajax.options.url}: ${
    +          e.message
    +        }`,
    

    These are unrelated changes. They removed if I run yarn prettier. So this should be run on this patch. https://www.drupal.org/node/2986680

  4. +++ b/core/misc/ajax.es6.js
    @@ -1562,5 +1566,29 @@
    +        document.querySelector(response.messageWrapperQuerySelector),
    

    response.messageWrapperQuerySelector needs to be documented in the doc block.

  5. +++ b/core/misc/message.es6.js
    @@ -162,6 +162,9 @@
    +      if (!this.messageWrapper) {
    +        this.messageWrapper = Drupal.Message.defaultWrapper();
    +      }
    

    I think this is fixed in #3052526: Drupal.Message.clear throws exception if messageWrapper not initialized if we can get that in first

  6. +++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestMessageForm.php
    @@ -0,0 +1,113 @@
    +class AjaxTestMessageForm extends FormBase {
    

    Nothing from FormBase is actually being used if we change this to implements FormInterface

    the test still passes

  7. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    @@ -0,0 +1,62 @@
    +    $assert_session->waitForElementVisible('css', '#alternate-message-container .messages--status:contains("I am a message in an alternate location.")');
    +    $assert_session->pageTextNotContains('I am a message in the default location.');
    

    waitForElementVisible() doesn't actually fail if the element never appears it just returns NULL.

    So these lines pass even if you update \Drupal\ajax_test\Form\AjaxTestMessageForm::makeMessageAlternate() to not pass the optional arg $messageWrapperQuerySelector to the command.

    If the first line is just wrapped in $this->assertNotEmpty() then the pageTextNotContains() is not needed.

    there are 2 other cases like this in the test.

  8. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    @@ -0,0 +1,62 @@
    +    for ($i = 0; $i < 6; $i++) {
    

    Super nit. If the for loop started with $i = 1 we don't have to test for $i+1. Would also have to change to $i <= 6

  9. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    @@ -0,0 +1,62 @@
    +      $assert_session->assertWaitOnAjaxRequest();
    +      $assert_session->elementsCount('css', '#alternate-message-container .messages', $i+1);
    

    I think anytime we can avoid assertWaitOnAjaxRequest() we should. I think it is better to explicitly wait for the thing we are expecting.

    This would work

    $all_elements_found = $page->waitFor(10, function () use ($i, $page) {
      return count($page->findAll('css','#alternate-message-container .messages')) === $i+1;
    });
    $this->assertTrue($all_elements_found);
    
  10. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
    @@ -6,7 +6,7 @@
    - * Tests core/drupal.messages library.
    + * Tests core/drupal.message library.
    

    This patch is not touching this file for anything so it feels unrelated. But good fix if we can get it in in this patch

oknate’s picture

Assigned: Unassigned » oknate

I'll make these changes this evening. Thanks for the review!

tedbow’s picture

Also I should have mentioned I think this is great new Ajax command!

phenaproxima’s picture

oknate’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new9.58 KB
new12.65 KB

Responding to #16:
1. ✅ Changed to "@var bool"
2. ✅ Added "@see Drupal.Message.defaultWrapper()"
3. ✅ Removed lines.
4. ✅ Added response.messageWrapperQuerySelector to the doc block.
5. 🎉 Removed this code!
6. ✅ changed to "implements FormInterface".
7. ✅ Updated to assert waitForElementVisible() doesn't return NULL. Since the check is repetitive, I added a helper function.
8. ✅ Updated loop to start at 1. Yes, that's nicer. Why didn't I think of that?
9. ✅ Thanks for the code there. Works great.
10. 👍

phenaproxima’s picture

Issue tags: +Needs change record

This is going to need a change record for sure.

tedbow’s picture

Status: Needs review » Needs work

@oknate thanks all the changes look good.

Sorry I didn't catch 1 other thing before and 1 could have

  1. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    @@ -0,0 +1,96 @@
    +    // Test that if MessageCommand::clearPrevious is FALSE, messages will not be cleared.
    

    This comment should wrap at 80 characters

+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestMessageForm.php
@@ -0,0 +1,113 @@
+    $response->addCommand(new MessageCommand('I am a message in the default location.'));
+
+    return $response;

super nit and probably doesn't need fixing. But if you agree and are fixing the other point

You can just return the result of addCommand() as this returns the response itself

oknate’s picture

Status: Needs work » Needs review
StatusFileSize
new2.43 KB
new12.6 KB

Fixed nits in #22.

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

@oknate thanks. Looks good!

Setting to RTBC! 🎉

Test fail will bump but I think it will be good!

tedbow’s picture

Status: Reviewed & tested by the community » Needs work

Sorry forgot that we still need the change record. but the patch looks good

oknate’s picture

Status: Needs work » Needs review

Here's the change record:
https://www.drupal.org/node/3086403

oknate’s picture

Issue tags: -Needs change record

When committed, make sure @bjmnm gets credit for doing the initial development on this!

phenaproxima’s picture

Crediting @bnjmnm per #9.

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs subsystem maintainer review

Change record looks good and detailed! Restoring RTBC as per #24, and removing the "Needs subsystem maintainer review" tag.

Thanks, @oknate and @tedbow! Let's land this.

phenaproxima’s picture

Turns out @tedbow is not, according to MAINTAINERS.txt, a subsystem maintainer of the AJAX system. Whoops.

So...I'm restoring the "needs subsystem maintainer review" tag, but keeping RTBC. And I'm assigning this to @effulgentsia who, as a subsystem maintainer and framework manager, is probably in the best position to review this patch.

phenaproxima’s picture

Also crediting @tedbow for reviews.

wim leers’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: -Needs subsystem maintainer review

I added Needs subsystem maintainer review specifically for the "message" aspect. I said that in #14, but should've been more explicit probably. I even quoted specifically those parts that I thought needed their sign-off.

The patch does not change the AJAX system. I'm a pseudo maintainer of the AJAX system too (having worked on the "AJAX page state" stuff a lot, as well as BigPipe, which uses the AJAX system too). So I feel comfortable reviewing this new AJAX command too.


  1. +++ b/core/lib/Drupal/Core/Ajax/MessageCommand.php
    @@ -0,0 +1,85 @@
    +   * @param string|null $messageWrapperQuerySelector
    ...
    +   * @param array $messageOptions
    ...
    +   * @param bool $clearPrevious
    ...
    +  public function __construct($message, $messageWrapperQuerySelector = NULL, array $messageOptions = [], $clearPrevious = TRUE) {
    

    🐪 These should not use camelCase. Only class properties use camelCase.

  2. +++ b/core/lib/Drupal/Core/Ajax/MessageCommand.php
    @@ -0,0 +1,85 @@
    +  public function getAttachedAssets() {
    +    $assets = new AttachedAssets();
    +    $assets->setLibraries(['core/drupal.message']);
    +    return $assets;
    +  }
    

    👍 AnnounceCommand uses the same pattern.

  3. +++ b/core/misc/ajax.es6.js
    @@ -1562,5 +1562,31 @@
    +     * Command to use Drupal.Message() via AJAX command.
    

    👎 Command to … via AJAX command. I think we can do better. What about Command to add a message to the message area.

  4. +++ b/core/misc/ajax.es6.js
    @@ -1562,5 +1562,31 @@
    +     *   The zone where to add the message. If null a default will be used.
    

    🤓 a default

    the default
    
  5. +++ b/core/misc/ajax.es6.js
    @@ -1562,5 +1562,31 @@
    +     *   If TRUE, clear previous messages.
    

    🤓 TRUEtrue (we only capitalize it in PHP, not in JS)

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    @@ -0,0 +1,97 @@
    +    }
    +
    +  }
    

    🤓 Extraneous blank line.

oknate’s picture

StatusFileSize
new3.79 KB
new12.54 KB

Addressing feedback in #33
1. ✅I also renamed the properties a bit. Using 'message' in the property names internally to this class is redundant.

-  protected $messageOptions;
+  protected $options;

etc.
3. ✅ Referencing AJAX command in an ajax command in ajax.es6.js is also redundant, so I like this better. 👍
4. ✅Good point, I misread the Drupal.Message()::defaultWrapper code. There's only one default. The test coverage demonstrates this too.
5. ✅Fixed.
6. ✅Fixed.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
phenaproxima’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/misc/ajax.es6.js
@@ -1562,5 +1562,31 @@
+    message(ajax, response) {
+      const messages = new Drupal.Message(
+        document.querySelector(response.messageWrapperQuerySelector),
+      );
+      if (response.clearPrevious) {
+        messages.clear();
+      }
+      messages.add(response.message, response.messageOptions);
+    },

So, I see a problem here. The drupal.ajax library does not have a dependency on drupal.message, which means that this might result in a "Drupal.Message is undefined" error unless some other code has coincidentally added the drupal.message library to the page.

I imagine we do NOT want to have drupal.ajax depend on drupal.message, so I would suggest that we modify messages.es6.js so that, if Drupal.AjaxCommands is defined, it adds the `message` callback to Drupal.AjaxCommands.prototype. Something like:

// in messages.es6.js

if (Drupal.AjaxCommands) {
  Drupal.AjaxCommands.prototype.message = (ajax, response) {
    // ...do the magic!
  }
}
tedbow’s picture

re #36

The current pattern in the patch is the same one we used for #3020352: Create a new AnnounceCommand to call Drupal.announce on an AjaxResponse
Drupal.AjaxCommands.announce() should only ever be called by invoking the \Drupal\Core\Ajax\AnnounceCommand on the server and \Drupal\Core\Ajax\AnnounceCommand::getAttachedAssets() ensures that the core/drupal.announce library is attached.

drupal.ajax does not depend on drupal.announce but we can be assured the library will be attached if Drupal.AjaxCommands.announce() is called.

This is the same with drupal.ajax not depending on drupal. message. We can be assured that if Drupal.AjaxCommands.message() is called the drupal. message library will be loaded because \Drupal\Core\Ajax\MessageCommand::getAttachedAssets() includes it.

All of the commands including the announce and message commands on Drupal.AjaxCommands require

* @param {Drupal.Ajax} [ajax]
     *   {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
     * @param {object} response
     *   The response from the Ajax request.

A developer would have to jump through a ton of hoops and really be making some bad decisions to actually construct these arguments from scratch outside of the server-side ajax command system that Drupal provides and call any of these JS command functions directly.
So I think it is safe to assume that any libraries the server-side command classes provide will be attached.

phenaproxima’s picture

Status: Needs work » Reviewed & tested by the community

This is the same with drupal.ajax not depending on drupal. message. We can be assured that if Drupal.AjaxCommands.message() is called the drupal. message library will be loaded because \Drupal\Core\Ajax\MessageCommand::getAttachedAssets() includes it.

Ah, okay. This is the thing I missed. Carry on!

andrewmacpherson’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Accessibility

Just seeing this for the first time. Spotted it referenced in #3081526: Add announcement after clicking 'Save and select' in the media library..

What happens if an ajax response includes both a MessageCommand and an AnnounceCommand. They will both result in calls to Drupal.announce(), right?

In the old issue that introduced Drupal.message, I pointed out that the use of Drupal.announce is less than ideal, because:

  • Anything can call it at any time. It's a free-for-all.
  • Priorities get mixed up.
  • Depending on timing, messages may be concatenated, or overwrite each other.
  • Concatenating lots of messages at once can be unreliable, because any keypress from the user may cancel all of the announcements. Users can carry out actions quicker than it takes to speak the announcements. There's a risk users can miss important information because it comes late in a long announcement.

The possibility for an ajax responses to have a MessageCommand and an AnnounceCommand could get flaky and confusing if these aren't used carefully.

Drupal.announce() should be used sparingly, I think. However I've noticed a trend over the last year or so towards treating it as the go-to solution for all your screen reader needs, and dumping arbitrary messages. Live regions shouldn't be treated like console output; that's not how they work at all (well, role="log" is intended to, but I don't think it's well supported yet).

In the longer term, I think we should move Drupal.message off of Drupal.announce, so it treats messages as live regions in their own right. However we may need to wait for browser and assistive tech support to improve first. In theory, the benefit is that the assistive tech can handle a queue of multiple ARIA live regions, possibly based on user preferences.

In the shorter term, it would be good to provide some kind of guidance about the difference between AnnounceCommand and MessageCommand. In many cases you will want one or the other; seldom both. I don't fully know what that guidance should look like yet, but at minimum it would be along the lines of:

"Developers should take care when using MessageCommand and AnnounceCommand together in the same AJAX response. Manual testing with screen readers is recommended."

Can we add something like that to change record, and class docs?

wim leers’s picture

Issue tags: +needs documentation updates, +Needs change record updates

Wow, I'm so glad you spotted that @andrewmacpherson! 😳 I should've thought of that too. Agreed with your recommendation!

oknate’s picture

An announce happens along with Drupal.Message::add() unless the options array is passed with options.announce is set to ''.

phenaproxima’s picture

Issue tags: +Needs tests

I would suggest this text for the class docs:

Developers should be extra careful if this command and \Drupal\Core\Ajax\AnnounceCommand are included in the same response. Unless the `announce` option is set to an empty string (''), this command will result in the message being announced to screen readers. When
combined with AnnounceCommand, this may result in unexpected behavior. Manual testing with a screen reader is strongly recommended.

Here are examples of how to suppress announcements:
@code
$command = new MessageCommand("I won't be announced", NULL, [
  'announce' => '',
]);
@endcode

@see \Drupal\Core\Ajax\AnnounceCommand

Also updating for explicit test coverage of this allowance.

oknate’s picture

Status: Needs work » Needs review
Issue tags: -needs documentation updates, -Needs change record updates, -Needs tests
StatusFileSize
new6.52 KB
new14.55 KB
  • This patch adds documentation per #39 and #42.
  • I updated the change record documentation. https://www.drupal.org/node/3086403
  • I verified that announcements can be suppressed and added test coverage for it in this patch.
  • I renamed the test class and form, because I kept forgetting what they are called. I put the name of the command 'MessageCommand' in the test name and form name, so they're easier to find. It's a DX improvement, IMHO.
    rename from core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    rename to core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
    index 872d42ed32..c9fe915d97 100644
    --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
    +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
oknate’s picture

StatusFileSize
new882 bytes
new14.56 KB

Small comment change in a helper method.

-   * Checks for inclusion of text in #drupal-live-announce.
+   * Checks for absence of the given text from #drupal-live-announce.
tedbow’s picture

Status: Needs review » Reviewed & tested by the community

@andrewmacpherson thanks for bring this up.

@oknate I think the changes look good

Back to RTBC!

andrewmacpherson’s picture

Status: Reviewed & tested by the community » Needs work

The latest patch adds advice to the MessageCommand class docs.

Shouldn't we also add corresponding advice to the AnnounceCommand class too?

oknate’s picture

Status: Needs work » Needs review
StatusFileSize
new884 bytes
new15.43 KB

Good point, @andrewmacpherson. Added a note on that command as well.

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community
larowlan’s picture

  • larowlan committed 7dde35c on 8.8.x
    Issue #3086096 by oknate, phenaproxima, tedbow, Wim Leers,...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed 7dde35c and pushed to 8.8.x. Thanks!

Published the change record

Status: Fixed » Closed (fixed)

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