By penyaskito, 23 May, 2021

There are lots of ways to learn about something, and everyone learns in a different way, having best results with different options: some people get better results with articles, others with videos, or others with examples. My favorite way is with examples. And when trying to learn how to achieve something with Drupal, my favorite examples are Drupal core itself.

If you need to get something done and not sure where to start, try to think: where have I seen that before?

You might even have an example in your codebase already. This article covers a real use-case I faced with Drupal Commerce, just simplifying it a bit for the sake of keeping it short. The user story is something alike: As a customer, I need to refresh the cart page instead of continuing checkout after editing the units of any order line, so I can see any better discounts that apply because of volume.  

So in terms of our form, this will mean: when in the Cart form page, disable the Checkout button when the user edits any quantity field, and enable the Update cart one.

Screenshot of the Shopping cart before editing it
Drupal commerce shopping cart form after applying some ugly styling with the purpose of having better differentiation between disabled and enabled buttons.

But for a better user experience, we want the user to notice which line item they modified. Where have I seen that before? In the locale translations interface.

If you enable the locale module, and add a second language to your site, you have an interface translation page where you can fill the translations of the interface strings.

The locale translation interface already mark edited translations, and that's in Drupal core
The locale translation interface UI already marks edited translations, and that's a valid example in Drupal core.

So we will look at the code and inspect how that's achieved. For me, the easiest way if you are not familiar with this piece of code is searching the UI strings or the url path of that page in my IDE, and dig until I find how that's working. In this case, if you search for "Changes made in this table" you end up in web/core/modules/locale/locale.admin.js, which is part of the drupal.locale.admin library defined at core/modules/locale/locale.libraries.yml. If you search for that library, you end up finding some forms that include the library with:

$form['#attached']['library'][] = 'locale/drupal.locale.admin';

So that's everything you need.

Let's create our module. Fastest way is using drush, so I executed

ddev drush generate module-standard

And picked commerce_cart_forcerefresh as the name. From the next options, I only generated the libraries file, which I edited to look like:

commerce_cart_forcerefresh:
  js:
    js/commerce-cart-forcerefresh.js: {}
  css:
    component:
      css/commerce-cart-forcerefresh.css: {}
  dependencies:
    - core/jquery
    - core/drupal
    - core/drupal.form
    - core/jquery.once

 Our css will only be

form#views-form-commerce-cart-form-default-1 tr.changed {
  background: #ffb;
}

And our JavaScript:

(function ($, Drupal, drupalSettings) {
  'use strict';

  Drupal.behaviors.cartForceRefresh = {
    attach: function (context) {
      var $form = $('form#views-form-commerce-cart-form-default-1').once('cartItemDirty');
      var $updateSubmit = $('form#views-form-commerce-cart-form-default-1 #edit-submit').prop('disabled', true);

      if ($form.length) {
        $form.one('formUpdated.cartItemDirty', 'table', function () {
          var $marker = $(Drupal.theme('cartItemChangedWarning')).hide();
          $(this).addClass('changed').before($marker);
          $marker.fadeIn('slow');
        });

        $form.on('formUpdated.cartItemDirty', 'tr', function () {
          var $row = $(this);
          var marker = Drupal.theme('cartItemChangedMarker');

          $row.addClass('changed');

          var $updateSubmit = $('form#views-form-commerce-cart-form-default-1 #edit-submit').prop('disabled', false);
          var $checkoutSubmit = $('form#views-form-commerce-cart-form-default-1 #edit-checkout').prop('disabled', true);

          if ($row.length) {
            $row.find('td:first-child .js-form-item').append(marker);
          }
        });
      }
    },
    detach: function (context, settings, trigger) {
      if (trigger === 'unload') {
        var $form = $('form#views-form-commerce-cart-form-default-1').removeOnce('cartItemDirty');
        if ($form.length) {
          $form.off('formUpdated.cartItemDirty');
        }
      }
    }
  };
  $.extend(Drupal.theme, {
    cartItemChangedMarker: function cartItemChangedMarker() {
      return '*';
    },
    cartItemChangedWarning: function cartItemChangedWarning() {
      return '
' + Drupal.theme('cartItemChangedMarker') + ' ' + Drupal.t('Update the cart for the changes to take effect.') + '
'; } }); })(jQuery, Drupal, drupalSettings);

Note that the form id is being used in some parts of that code, and that depends on the view id. Take that into account if you are not using the default view, you have multiple line item types, etc. In my real case, I was able to use a class that my theme was adding to that form.  

The last thing is ensuring this behavior is added to our form. The easiest for this demo is using hook_form_FORMID_alter(). Our commerce_cart_forcerefresh.module:

And that produces the desired result:

Final result with order line items cues when they were edited.
The Checkout button is disabled, the Update cart button is enabled, a cue is added on the line item, and a warning on the top of the page.

Hope this helps!