Problem/Motivation
When you click a thread in the inbox, AjaxController::loadThread() renders the thread and sends back only its drupalSettings, through a SettingsCommand. The thread's #attached libraries never reach the page. Anything in the thread that depends on a library the page does not already have renders unstyled and without its behaviors.
This came in with #3054106: Javascript files loaded twice when clicking on thread link in PM block. Back then the thread JS loaded threads with a plain $.ajax() call, which sends no ajax_page_state, so the attachments processor treated every library as new and sent all of them again. The fix there dropped the attachments and kept the settings. Since then the thread JS moved to Drupal.ajax() (private_message_thread.js, loadThread()), and core adds ajax_page_state to every Drupal.ajax() request (core/misc/ajax.js, beforeSerialize()). The original cause is gone and the side effect is still there.
We hit it with a reaction field (votingapi_reactions) on private messages. Open the messages page with no thread selected, click a thread in the inbox, and the reaction icons collapse to nothing because the formatter's CSS is missing.
The other callbacks in AjaxController render to a string the same way (getNewPrivateMessages(), getOldPrivateMessages(), getOldInboxThreads() and getNewInboxThreads()), so a new message or older messages that need a library the page lacks have the same problem.
Steps to reproduce
- Add a field to the
private_messageentity whose formatter attaches a library, and show it on the default display. - Create a thread with a message that has a value in that field.
- Log in as a member of that thread and open the messages page with no thread selected.
- Click the thread in the inbox block.
- The field renders without its CSS. The
load_threadresponse holds onlysettings,privateMessageInsertThreadandprivateMessageUpdateUnreadItemsCountcommands.
Proposed resolution
Hand the thread's attachments back to the response, and let core's AjaxResponseAttachmentsProcessor work out what the page still needs. It reads the libraries listed in ajax_page_state and only adds CSS and JS the page does not have, so nothing loads twice.
$rendered_thread = (string) $this->renderer->renderRoot($renderable); $response->setAttachments($renderable['#attached']); $response->addCommand(new PrivateMessageInsertThreadCommand($rendered_thread));
The SettingsCommand can go, since the processor sends the settings too. The same change applies to the other callbacks that insert rendered markup.
This is the pattern core uses for every AJAX render, AjaxRenderer::renderResponse() calls $response->setAttachments($main_content['#attached']). The module already does it in PrivateMessageForm, where the AJAX submit calls $response->setAttachments($new_form['#attached']) before replacing the form.
The MR adds two tests to InboxBlockTest, both clicking a thread with the existing clickThread() helper. The first uses a test module whose field formatter attaches a library, and checks its CSS is on the page after the thread loads, which proves the bug by failing without the fix. The second checks private_message_thread.js and the test library's files are each on the page exactly once after loading a thread, and again after switching to a second thread and back. That is the regression #3054106: Javascript files loaded twice when clicking on thread link in PM block fixed, so it keeps the double loading from coming back.
Remaining tasks
- MR with the change and the two tests.
Issue fork private_message-3626104
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
loze commentedI opened !220 against 4.x.
The MR puts the rendered markup's
#attachedback on the response in everyAjaxControllercallback,loadThread()and the four that insert messages or inbox threads. Core's attachments processor then sends only the CSS and JS the page does not have yet, going by theajax_page_statethatDrupal.ajax()sends with every request.loadThread()no longer needs itsSettingsCommand, since the settings come through the same way.It adds two tests to
InboxBlockTestand a small test module,private_message_assets_test, which attaches a library to one thread only. The first test opens the messages page on another thread, clicks that one in the inbox, and checks the library's CSS and JS arrive. The second switches threads three times and checksprivate_message_thread.jsand the test library are each on the page once, which covers the double loading from #3054106: Javascript files loaded twice when clicking on thread link in PM block. Locally the first fails without the fix and both pass with it, along with the three tests already inInboxBlockTest. When I made the controller ignoreajax_page_state, the second one failed with two copies ofprivate_message_thread.js.The pipeline stops at the composer job, the same as 4.x itself: the module's
composer.jsonpinsdrupal/coderto8.3.26, and core-dev 11.4.6 needs^8.3.30. #3549600: Fix pipeline (!209) loosens that pin. Once it lands, this MR should go through the rest of the pipeline.Comment #4
loze commented