Paging a view with ajax enabled currently breaks Add to Cart forms & variation field updates.

Attempting to modify the ajax-loaded products attributes will cause the ajax request, to update the attributes, to fail with a 404 to /views/ajax.

Attempting to add the ajax-loaded product to the cart (even without modifying the attributes) redirects to /views/ajax, which renders the 404 page.

I have created a failing test case to show the error.

I'm trying to diagnose the cause of the issue, but could use some advice on where to look. I'm inspecting the ProductVariationAttributesWidget, initial elements and ajax loaded elements both seem to look the same. It seems like the form actions are getting replaced with the views form actions, but if so, I'm not sure when that is happening. I've stepped through and haven't seen the element's #ajax property change.

Comments

thechanceg created an issue. See original summary.

bojanz’s picture

Views steals the form, basically. This is the same problem that existed in D7, and I don't think we ever found a workaround.
You can try searching on drupal.org for clues. I found this: https://www.drupal.org/node/2185239#comment-8431647 which confirms my impression.

mglaman’s picture

Linking to PR which has failing test to at least cover this problem: https://github.com/drupalcommerce/commerce/pull/815

thirstysix’s picture

In Drupal8 Views, I have also the same problem on views ajax request. Any idea?

cybercraft2003’s picture

I've the same problem. I tried to disable Ajax on view and custom ajax works.

drugan’s picture

@Cybercraft2003

Could you please share your "custom ajax" code.

cybercraft2003’s picture

@drugan

$response = new AjaxResponse();

  $storage = $form_state->getStorage();

  // Get the cart
  $cart = Order::load($storage['cart_id']);
  $cart_items = $cart->getItems();
  $item = array_pop($cart_items);

  // Get last added product
  $product = $item->getPurchasedEntity();
  $product = $product->getProduct();

  // Reduce duplication
  foreach ($cart_items as $cart_item) {
    // Get the product from current order item;
    $cart_product = $cart_item->getPurchasedEntity();
    $cart_product = $cart_product->getProduct();
    // Check id for reduce duplication
    if ($cart_product->id() == $product->id()) {
      $li_qt = $item->getQuantity();
      $ci_qt = $cart_item->getQuantity();
      // Calc the new quantity
      $newqt = (float) $ci_qt + (float) $li_qt;
      // Add the new quantity
      $cart_item->setQuantity($newqt);
      // Delete the duplicate item
      $cart->removeItem($item);
      // And save the cart
      $cart->save();
    }
  }

  // Set correct TVA
  $cart = Block::load('panier');
  $render = \Drupal::entityTypeManager()
    ->getViewBuilder('block')
    ->view($cart);

  $response->addCommand(new ReplaceCommand('.cart--cart-block', $render));
  $response->addCommand(new ReplaceCommand('.' . $form['#attributes']['class'][0], [
    '#value' => t('Produit ajouté!'),
    '#type' => 'button',
    '#attributes' => [
      'class' => ['add-to-cart-submit', $form['#attributes']['class'][0], 'product-added'],
      'disabled' => true
    ]
  ]));
  return $response;

This code is in ajax callback for submit button in form with id : 'commerce_order_item_add_to_cart_form'

drugan’s picture

@Cybercraft2003

Thank you. I've tried to solve this issue earlier but no luck. A bit later I'll test your solution.

cybercraft2003’s picture

@drugan The "bug" appear when you run ajax on view. The first build of form work fine. In my case, the bug occur when i go to another page with the classic view pager on my catalog view.

drugan’s picture

Hmm.., that means you also had not fixed this issue. The reason is that views modifies the form action though not actually using this target address. If you'd look at the address bar the path (my-products-page) stays the same while navigating with pager, though the action is changed to /views/ajax. But, if you hover on the pager links you'll see something like my-products-page?page=2. I think it is for the case when javascript is disabled, so the view will work with a normal page refresh.

What we need basically find a method to modify our product forms actions to a product/NUMBER back again and at the same time do not break the views ajax pager. Struggled with this but as always other issues had taken away me from the process....

drugan’s picture

This issue is now fixed with the following patch:

https://github.com/drupalcommerce/commerce/compare/8.x-2.x...drugan:allo...

It is quite a complex solution and besides the current issue fixes these ones:

#2707721: Incorrect display of attribute field values on the Add To Cart form
#2730643: Product variation attributes shows combines variations that not exist.
#2755529: Product variant bulk creation
#2831739: Allow combined attributes

Actually, the patch includes both the variation bulk creator and combined attributes patch with some additions to fix issues like the current one.

bojanz’s picture

@drugan
You are aware that your tendency to merge multiple fixes into one patch (each of which is often overcomplicated on its own), while having no failing test, is the reason why they never get committed, right?

drugan’s picture

Yes, I understand this but who gives a guarantee that after spending a lot of time to break the solution into pieces and writing tests they will not be thrown into the bin. Time is money. I've written the solution for myself and shared it in a hope that this will be useful for some people.

As for the "overcomplicated on its own" not only mine solutions look like that way. It is pretty difficult to fix complex interrelated issues just writing a couple lines of code. I'd be glad to look at an example.

nchar’s picture

This solution works for commerce_product views but doesn't work for search_api_index views that render products entities.

Also this issue may be related: #2927739: RefreshLess breaks the commerce add to card form

nchar’s picture

In the last submitted patch I think that the line:

$path = trim(explode('?', $view->getPath())[0], '/');

should be replaced with:

$path = $view->getUrl()->toString();

because in my case the path of the view is "taxonomy/term/%taxonomy_term" so I need the actual url with the replaced parameters.

akalam’s picture

Status: Active » Needs work

I agree with drugan, I don't see any problem with a patch that solves multiple issues when those are related to a single code bug (although it affects to multiple functionalities ). Thanks for your time and the approach.
I tried the patch with the nchar suggestion. Anyway the patch doesn't work for block displays. With patch applied, block displays with ajax enabled don't render at all

nchar’s picture

Something like that may also work with blocks:

$path = $view->hasUrl($args, $display_id) ? $view->getUrl()->toString() : trim(explode('?', $view->getPath())[0], '/');

drugan’s picture

As for me the ajax paging in block also worked but tested the #17 suggestion I had not found any regression, so it was added into the patch:

https://github.com/drupalcommerce/commerce/compare/8.x-2.x...drugan:allo...

Thanks @nchar. Needs more testing and feedback. The only caveat is that if you have both page and block displays on the view then after adding variation to cart from within a block you will be redirected to the the appropriate page display like as https://example.com/my-products?page=3. So, the workaround is to have just one either block or page display on the view.

Regarding RefreshLess I think we need to make commerce products to work properly in views first and only after that diving into such specific use cases.

nchar’s picture

I have #17 in production sites for a while and I came up with a small improvement. In some cases (multilingual websites), the path includes the language prefix eg. /en/taxonomy/term/15. This causes ajax errors.

The improvement is this:

$path = $view->hasUrl($args, $display_id) ? $view->getUrl()->getInternalPath() : trim(explode('?', $view->getPath())[0], '/');

So getInternalPath insted of toString solves the issue.

christian-heiko’s picture

i wrote this hook from the huge patch which fixed the issue for my project:

/**
 * Fixes «Ajax Paging in Views breaks Add to Cart form»  https://www.drupal.org/project/commerce/issues/2916671
 * Implements form_commerce_order_item_add_to_cart_form_alter
 */
function MODULE_NAME_form_commerce_order_item_add_to_cart_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $request = \Drupal::request();
  $views_ajax = preg_match( '{views(\.|/)ajax}', $request->getRequestUri());
  $route = $request->attributes->get('_route') == 'views.ajax';
  if (($views_ajax || $route) && $request->headers->has('referer')) {
    $referer = $request->headers->get('referer');
    $action = str_replace($request->getSchemeAndHttpHost(), '', $referer);
    // Page is somehow needed, otherwise the product will not be added to the cart
    $page = $request->query->get('page');
    $form['#action'] = $action . ( $page ? '?page=' . $page : '' );
  }
 }
goz’s picture

I found a solution thanks to javascript. I handle .beforeSend to replace remove views/ajax when submission is about my cart form.

Tests are based on my own addtocart specifications, but you can base yours on the form id.

See commit in https://www.drupal.org/project/commerce_addtocart_ajax/issues/3115072

rossb89’s picture

I've had this same issue here (I think).

Context: a search api view, block display, ajax enabled, that shows rendered teaser's of commerce product's with the add to cart form visible on each teaser.

After applying a sort on the view through views ajax, the form action of the add to cart form on each teaser gets changed to /views/ajax.

Looks like the committed core issue 2820347 solves this issue for exposed filters in ViewsExposedForm by checking if the route is views.ajax then falling back to using the path.current service and setting the form action to the result of getPath(). - see the commit here

So applying the same kinda logic here would look something like this code snippet below

function MODULE_NAME_form_commerce_order_item_add_to_cart_form_alter(&$form, FormStateInterface $form_state, $form_id) {
    // .. any other logic you have ... 
    // Ensure the form action is always the current page URL.
    if (Drupal::routeMatch()->getRouteName() == 'views.ajax') {
      $path_current = \Drupal::service('path.current');
      $form['#action'] = Url::fromUserInput($path_current->getPath())->toString();
    }
}

I had issues with this to start with ... even though this was now setting the form action correctly and the form appeared to be submitting it was not correctly - after applying a sort to the view, the product never got added to the cart and the page just refreshed.
A few hours later going deep with Xdebug and it turned out the issue was quite simply that the product that I was trying to add to the cart wasn't present on the submitted page url that I was specifying as the form action because the action was missing the filter / sorting params from the URL.

So refactored this to the following:

function MODULE_NAME_form_commerce_order_item_add_to_cart_form_alter(&$form, FormStateInterface $form_state, $form_id) {
    // .. any other logic you have ... 
    // Ensure the form action is always the current page URL.
    if (Drupal::routeMatch()->getRouteName() == 'views.ajax') {
      $path_current = \Drupal::service('path.current');
      $action_url = Url::fromUserInput($path_current->getPath())->toString();

      // Add on any request parameters.
      $request = \Drupal::request();
      $request_all = $request->request->all();
      unset($request_all['ajax_page_state']);

      $query = http_build_query($request_all, '', '&');
      $form['#action'] = $action_url . '?' . $query;
    }
}

Now, the form is submitting to the correct page URL and because the product in question (that was only visible after sorting) is present there, the add to cart submit runs correctly :)

harcher’s picture

@rossb89

You're the man! Thank you for this great contribution. I had the same issue and added your snippet into a custom module and now I am back on track. All works! Thanks buddy!

renrhaf’s picture

@rossb89

Also a big fan of your snippet here ! Thanks !
The problem remains for one specific case for me : I display random products in a view, and this product may not appear on the page when it's reloaded when adding to the cart. Thus the product is not added to the cart. I'll create some custom code to use AJAX behavior for adding to cart there.

lubwn’s picture

I had kind of similar problem or maybe my problem was rooting from the same underlaying code, however I was adding products to cart with custom ajax and afterwards reloading cart view with javascript $('.view-commerce-cart-block').trigger('RefreshView');.

My cart form had "remove product from cart" button, meaning it was rendered form, however due to my custom JS reload of the form it was now pointing to /views/ajax. Clicking the "remove product" button then lead to error 404 as a redirect from /views/ajax handled by drupal itself.

But I noticed that despite the wrong redirect, it was actually removing product from the cart successfully. So I tried to just swap the form action URL to current page and to my surprise it worked. My code is pretty simple:

$( document ).ajaxComplete(function() {

	// Fix for https://www.drupal.org/project/commerce/issues/2916671
	$('.view-commerce-cart-block form').attr("action", window.location.href);
	
});

It works and now and user is redirected back to the product. Quite a dirty fix.

sayco’s picture

It seems like the issue is related to the https://www.drupal.org/project/drupal/issues/2504115

I've figure out some sort of solution proposed here for a views module
https://www.drupal.org/project/drupal/issues/2504709#comment-15001764

which will allow us to access the original views_path that is sent over with the main request.
This is the first step you have to take.
The second step is to modify the preprocessing.
I've added my own implementation to the hook_preprocess_views_view, but for the commerce_cart module it could be more or less as follows:

function commerce_cart_preprocess_views_view(&$variables) {
  $view = $variables['view'];
  if (strpos($view->storage->get('tag'), 'commerce_cart_form') !== FALSE) {
    $variables['rows']['footer'] = $variables['footer'];
    $variables['footer'] = '';

    $request = \Drupal::requestStack()->getMainRequest();
    $is_ajax_reload = $request->isXmlHttpRequest() && $request->request->get('_drupal_ajax') === '1';
    if (FALSE === $is_ajax_reload) {
      return;
    }
    $variables['rows']['#action'] = $request->request->get('view_path');
  }
}

Seems to be working as expected now.

lawxen’s picture

We have similar ajax problem when using commerce_ajax_atc.
After filer product name. product which not in original first page can't be added successfully.
After some debugging, I found it caused by this code
$form_state->setUserInput($view->getExposedInput()); in ViewsExposedForm
Rerquest missing trigger_element*** which caused the input of exposed views be replaced.

Below is our tmp fix solution

/**
 * Implements hook_form_views_exposed_form_alter().
 */
function MODULE_NAME_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  if ($form['#id'] == 'views-exposed-form-commerce-products-products') {
    /** @var \Drupal\views\ViewExecutable $view */
    $view = $form_state->get('view');
    $view_id = $view->id();
    $view_display = $view->current_display;
    if ($view_id == 'commerce_products' && $view_display == 'products') {
      $form_state->setUserInput($_GET);
    }
  }
}