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.
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.
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. JavaScript files are included via libraries, so if you search for the filename you will hit the drupal.locale.admin library defined at core/modules/locale/locale.libraries.yml.
If you search now 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 involved in make this for the locale translation UI, now you need to mimic that.
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 '<abbr class="warning ajax-changed" title="' + Drupal.t('Changed') + '">*</abbr>';
},
cartItemChangedWarning: function cartItemChangedWarning() {
return '<div class="clearfix messages messages--warning">' +
Drupal.theme('cartItemChangedMarker') + ' ' +
Drupal.t('Update the cart for the changes to take effect.') + '</div>';
}
});
})(jQuery, Drupal, drupalSettings);
Note that the form id is being used in some parts of our css and javascript, and that depends on the view id. Take that into account if you are not using the default view, you have multiple order 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(), which again depends on the id of the view. Our commerce_cart_forcerefresh.module looks like:
<?php
use Drupal\Core\Form\FormStateInterface;
/**
* @file
* Primary module hooks for commerce_cart_forcerefresh module.
*/
function commerce_cart_forcerefresh_form_views_form_commerce_cart_form_default_1_alter(&$form, FormStateInterface $form_state, $form_id) {
$form['#attached']['library'][] = 'commerce_cart_forcerefresh/commerce_cart_forcerefresh';
}
And that produces the desired result:
Hope this helps! Full source code at https://github.com/penyaskito/drupal_commerce_cart_forcerefresh
You can read more about JavaScript behaviors in this guide: How to integrate JavaScript in Drupal 8/9, by davidjguru.