Magento2: Show messages only after page reload if I use ajax action

Created on 31 Aug 2017  路  16Comments  路  Source: magento/magento2

Some time before I recieve bug report from Marketplace QA testing team.

Preconditions

Magento 2.1.7 Community Edition (PHP 5.6.31)
Magento 2.1.7 Community Edition (PHP 7.0.21)

Steps to reproduce

Clicking on the "Add to compare" button on the catalog page only causes the button to switch to the loading animation.
For signed out users, the message that the product is added to compare only appears after a new page is loaded. However the product may not be added to compare.
For logged in users the product the message does not appear at all and the product is not added.

The fact is that I'm adding product to compare via ajax request. I conducted some investigation and I find reasons in commit : MAGETWO-46014: delayed error messages magento/magento2@541ec80.
Messages move from session to cookies.
https://github.com/magento/magento2/blob/develop/app/code/Magento/Theme/Controller/Result/MessagePlugin.php#L83 - not for json response

But messages do not appear after ajax. There is no such logic in messages component.
https://github.com/magento/magento2/blob/develop/app/code/Magento/Theme/view/frontend/web/js/view/messages.js

I have a patch for this
~~~js
diff --git a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js
index aafba52..d56b673 100644
--- a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js
+++ b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js
@@ -38,6 +38,14 @@ define([
}

         $.cookieStorage.set('mage-messages', '');

+

  • $(document).on('ajaxComplete', function (event, xhr, settings) {
  • var messages = $.cookieStorage.get('mage-messages');
  • if (!_.isEmpty(messages)) {
  • customerData.set('messages', {messages: messages});
  • $.cookieStorage.set('mage-messages', '');
  • }
  • });
    }
    });
    });
    ~~~
    Should I create pull request for it.

All 16 comments

The fact is that I'm adding product to compare via ajax request

Do you mean some custom action implemented?

The patch does not look good to me, you should invalidate compare-products section in customer data instead and it will fill automatically, just like core version does: https://github.com/magento/magento2/blob/develop/app/code/Magento/Catalog/etc/frontend/sections.xml#L11

Do you mean some custom action implemented?

I convert data-post to something like ajax-data-post.
Then I send the ajax request to the standart action.
Catch response then skip redirect and return simple json
~xml



~

screenshot from 2017-08-31 17-23-21

I see... It looks like your implementation is slightly different from core one, try to figure out what is the crucial difference.

messages section is invalidated on any action, the /customer/section/load/ response from your screenshot should contain the message already. ajaxComplete listener is declared here: https://github.com/magento/magento2/blob/develop/app/code/Magento/Customer/view/frontend/web/js/customer-data.js#L371

After catalog/product_compare/add action

screenshot from 2017-08-31 17-37-40

messages appear in cookie mage-messages, but /customer/section/load/ action return empty messages section
screenshot from 2017-08-31 17-39-07

so I propose to check the cookies mage-messages after each ajaxComplete

messages are supposed to be filled from cookie only once, during initialization. Can you reproduce this issue on a vanilla Magento 2 installation?

I understand what you want to say. Not reproduce on vanilla Magento 2. So this is not our problem.
so I add this patch in my code extensions, maybe even override messages.js component.

But there is one place (in 2.2-dev) in code where are you add message directly from ajax response
~js
customerData.set('messages'
~

https://github.com/magento/magento2/blob/4f034838eaddbebfc76bc640ff92d2cfd928eebb/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout.js#L43

You could get rid of this.

Thanks for the details.

True, the problem is in your implementation which does not correspond to core one. Overriding a whole component like messages.js is not a good idea. Try to figure out how the things are intended to work in core first.

Example from PayPal express checkout is perfectly valid except it could probably use app/code/Magento/Ui/view/frontend/web/js/model/messages.js instead of changing customer data. It is much better than random hack changing customer data on ajaxComplete. I believe that is the reason of not setting cookie in case of JSON response: messages should be in response instead of cookie and your custom component should handle this situation.

workaround
~~~js
define([
'jquery',
'uiComponent',
'Magento_Customer/js/customer-data',
'underscore',
/'Magento_Customer/js/section-config',/
'jquery/jquery-storageapi'
], function ($, Component, customerData, _/, sectionConfig/) {
'use strict';

$(document).on('ajaxComplete', function (event, xhr, settings) {
    if (settings.type.match(/get/i)
        && settings.url.match(/customer\/section\/load/i)
        && _.isObject(xhr.responseJSON)
        && xhr.responseJSON.messages
        && xhr.responseJSON.messages.messages
        && 0 === xhr.responseJSON.messages.messages.length
    ) {
        // var sections = sectionConfig.getAffectedSections(settings.url);
        // if (sections && _.contains(sections, 'messages')) {
            var messages = $.cookieStorage.get('mage-messages');
            if (!_.isEmpty(messages)) {
                customerData.set('messages', {messages: messages});
                $.cookieStorage.set('mage-messages', '');
            }
        // }
    }
});

});
~~~

@0m3r just another unnecessary dirty hack. If customer data/messages really does not work in some cases, it's better to improve core implementation.

I believe that is the reason of not setting cookie in case of JSON response: messages should be in response instead of cookie and your custom component should handle this situation.

you want me to do something like this. right
~~~diff
diff --git a/Observer/FixIsAjaxObserver.php b/Observer/FixIsAjaxObserver.php
index f24f68b..2fe81a1 100644
--- a/Observer/FixIsAjaxObserver.php
+++ b/Observer/FixIsAjaxObserver.php
@@ -5,6 +5,9 @@

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Json\Helper\Data as jsonDataHelper;
+use Magento\Framework\Message\ManagerInterface as MessageManager;
+use Magento\Framework\Message\MessageInterface;
+use Magento\Framework\View\Element\Message\InterpretationStrategyInterface;*/
use Zend\Http\Header\HeaderInterface as HttpHeaderInterface;

class FixIsAjaxObserver implements ObserverInterface
@@ -16,15 +19,32 @@ class FixIsAjaxObserver implements ObserverInterface
*/
protected $jsonHelper;

  • /**

    • Manager messages

  • *

    • @var MessageManager

  • */
  • protected $messageManager;
    +
  • /**

    • @var InterpretationStrategyInterface

  • */
  • private $interpretationStrategy;

    /**

    • Constructor
      *
    • @param jsonDataHelper $helper

    • @param MessageManager $messageManager


    • @param InterpretationStrategyInterface $interpretationStrategy

      */

  • public function __construct(jsonDataHelper $jsonHelper)
  • {
  • public function __construct(
  • jsonDataHelper $jsonHelper,
  • MessageManager $messageManager,
  • InterpretationStrategyInterface $interpretationStrategy
  • ) {
    $this->jsonHelper = $jsonHelper;
  • $this->messageManager = $messageManager;
  • $this->interpretationStrategy = $interpretationStrategy;
    }

    /*
    @@ -41,8 +61,20 @@ public function execute(Observer $observer)
    /
    * @var $response \Magento\Framework\App\ResponseInterface */
    $response = $controller->getResponse();
    if ($request->isAjax()) {

  • $messages = $this->messageManager->getMessages(true);
    $result = [
  • 'action' => $request->getFullActionName()
  • 'action' => $request->getFullActionName(),
  • 'messages' => array_reduce(
  • $messages->getItems(),
  • function (array $result, MessageInterface $message) {
  • $result[] = [
  • 'type' => $message->getType(),
  • 'text' => $this->interpretationStrategy->interpret($message)
  • ];
  • return $result;
  • },
  • []
  • ),
    ];
    $cacheControlHeader = $response->getHeader('Cache-Control');

diff --git a/view/frontend/web/js/ajaxcian-data-post.js b/view/frontend/web/js/ajaxcian-data-post.js
index a960a2a..b774218 100644
--- a/view/frontend/web/js/ajaxcian-data-post.js
+++ b/view/frontend/web/js/ajaxcian-data-post.js
@@ -1,8 +1,9 @@
define([
'jquery',
'AjaxproLoader',

  • 'Magento_Customer/js/customer-data',
    'jquery/ui'
    -], function ($, loader) {
    +], function ($, loader, customerData) {
    "use strict";
 $.widget('swissup.ajaxcianDataPost', {

@@ -60,13 +61,19 @@ define([
element.css({'color': 'transparent'});
loader.startLoader(element);
},
- success: function (res) {
+ success: function (response) {
element.css({'color': ''});
loader.stopLoader(element);
- if (res.backUrl) {
- window.location = res.backUrl;
+ if (response.backUrl) {
+ window.location = response.backUrl;
return;
}
+ var messages = response && response.messages;
+ if (messages) {
+ customerData.set('messages', {
+ messages: messages
+ });
+ }
}
})
.fail(function (jqXHR) {
~~~

then the message appears and immediately disappears after customer/section/load
it looks more foolish

ezgif com-video-to-gif 2

@0m3r try comparing to core implementation (it does not behave this way, correct?).

I don't see why returning messages in response is needed here. Just invalidate messages section on POST action.

messages invalidate automaticaly after each POST
but messages moving from session to cookie mage-messages.
https://github.com/magento/magento2/blob/develop/app/code/Magento/Theme/Controller/Result/MessagePlugin.php#L83 - not for json response

The idea is to use existing actions (comare add, remove, wishlist add etc.) like ajax. Just send ajax request on existed core actions. Problem is that though I change the response on json. But the $result I can't change.

And I found solution. Create plugin for Magento\Framework\App\Action\Action.
~xml



~

Plugin dispatches event with $result. $Result is a result of function execute(). (RedirectInterface, JsonInterface, null, etc. ).
~~~php
public function afterExecute(
$subject,
$result
) {
$request = $subject->getRequest();
$eventName = 'xxx_xxx_controller_action_after_execute_' . $request->getFullActionName();
$resultObject = $this->dataObjectFactory->create(['data' => ['result' => $result]]);
$eventParameters = [
'controller_action' => $subject,
'request' => $request,
'result_object' => $resultObject
];

    $this->eventManager->dispatch($eventName, $eventParameters);

    $newResult = $resultObject->getResult();
    if ($newResult instanceof AbstractResult) {
        $result = $newResult;
    }
    return $result;
}

~
now I can create observer for my custom event
~
xml



~
Check is ajax then replace result redirect to result json if need
~
php
/* RedirectResultToJsonResultObserver.php /
$event = $observer->getEvent();
/
* @var \Magento\Framework\App\RequestInterface $request /
$request = $event->getRequest();
$resultObject = $event->getResultObject();
$result = $resultObject->getResult();
if ($request->isAjax() && 1 == $request->getParam('ajax', false) && !($result instanceof Json)) {
/
* @var \Magento\Framework\Controller\Result\Json $resultJson */
$resultJson = $this->jsonResultFactory->create();
$resultJsonData = [
'action' => $request->getFullActionName()
];
$resultJson->setData($resultJsonData);
$resultObject->setResult($resultJson);
}
~~~

@0m3r the idea to use interception mechanism to make existing compare actions ajax-compatible is smart but dispatching new events and especially mix event-observers with plugins-interceptors is not a good idea.

Which part of this logic is not possible with plugins only?

@orlangur
with events it happened historically. I used
~php
'controller_action_postdispatch_' . $request->getFullActionName()
~

I hope I can use them again in the future without plugin when Magento team make response greate again.

Something like that.
~~~diff diff --git a/lib/internal/Magento/Framework/App/Action/Action.php b/lib/internal/Magento/Framework/App/Action/Action.php
index b68e7e8..bfb60b1 100644
--- a/lib/internal/Magento/Framework/App/Action/Action.php
+++ b/lib/internal/Magento/Framework/App/Action/Action.php
@@ -67,6 +67,11 @@ abstract class Action extends AbstractAction
protected $messageManager;

 /**


    • @var \Magento\Framework\Controller\ResultInterface

  • */
  • protected $result;
    +
  • /**

    • @param Context $context

      */

      public function __construct(Context $context)

      @@ -101,10 +106,10 @@ abstract class Action extends AbstractAction

      );

      \Magento\Framework\Profiler::start($profilerKey);

  • $result = null;
  • $this->result = null;
    if ($request->isDispatched() && !$this->_actionFlag->get('', self::FLAG_NO_DISPATCH)) {
    \Magento\Framework\Profiler::start('action_body');
  • $result = $this->execute();
  • $this->result = $this->execute();
  • //or $this->setResult($this->execute());
    \Magento\Framework\Profiler::start('postdispatch');
    if (!$this->_actionFlag->get('', self::FLAG_NO_POST_DISPATCH)) {
    $this->_eventManager->dispatch(
    @@ -121,7 +126,28 @@ abstract class Action extends AbstractAction
    \Magento\Framework\Profiler::stop('action_body');
    }
    \Magento\Framework\Profiler::stop($profilerKey);
  • return $result ?: $this->_response;
  • return $this->result ?: $this->_response;
  • }
    +
  • /**

    • Retrieve result object

  • *

    • @return \Magento\Framework\Controller\ResultInterface

  • */
  • public function getResult()
  • {
  • return $this->result;
  • }
    +
  • /**

    • Set result object

  • *

    • @return \Magento\Framework\Controller\ResultInterface

  • */
  • public function setResult(\Magento\Framework\Controller\ResultInterface $result)
  • {
  • $this->result = $result;
  • return $this;
    }

~~~

@0m3r I see... Not gonna happen, event-observers is just a legacy mechanism, would be better to disable it at all ;) Plugins-interceptors are great on the other hand, just need to get used to them a bit.

Was this page helpful?
0 / 5 - 0 ratings