Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

From Adding a Button to Change Detection

4.00/5 (1 vote)
18 May 2019CPOL3 min read 3.9K  
A simple thing may lead to so much

Introduction

When it comes to maintaining a project or fixing bugs, the first and important thing to keep in mind is not to introduce new bugs.

I was asked to add a Back button to modal popups to close them whenever it is clicked. The modal is required to look like this:

Image 1

It is in a Bootstrap modal popup. There are 7 modal body divs for the whole workflow. The Back button should be hidden in the first modal body, while displayed in all the others when the Continue button is clicked. The divs show or hide based on the workflow step. The HTML is excerpted as below:

HTML
<div class="modal fade in" data-refresh="true" tabindex="-1" 
     role="dialog" id="contact-modal" style="display: block;">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <span class="modal-title">Add Contact(s)</span>
            </div>
            <div class="modal-body modal-body-1" style="display: block;">...</div>
            <!-- These sections below hide/show as needed -->
            <div class="modal-body modal-body-2 d-none" style="display: none;">...</div>
            <div class="modal-body modal-body-3 d-none" style="display: none;">...</div>
            <div class="modal-body modal-body-4 d-none" style="display: none;">...</div>
            <div class="modal-body modal-body-5 d-none" style="display: none;">...</div>
            <div class="modal-body modal-body-6 d-none" style="display: none;">...</div>
            <div class="modal-body modal-body-7 d-none" style="display: none;">...</div>
        </div>
    </div>
</div> 

My First Fix

It was simple, I thought, of course, without thinking twice. First, I added a hidden Back button with bootstrap d-none css class to every modal body, a css class btn-continue to all the Continue buttons, and btn-cancel to all Cancel buttons. The Cancel buttons are made to hide the current visible modal body and show the previous one. The added css classes are just for ease to get the elements by jQuery. Then, write the JavaScript code:

JavaScript
var countPopup = 0;
$('#contact-modal').on('show.bs.modal', function () {
     countPopup = 0;
     ...... (existing code)
 }).on('hide.bs.modal', function (e) {
     countPopup = 0;
     ...... (existing code)
 });

$('button.btn-continue').on('click', function() {
     countPopup ++;
     $('button.btn-back').removeClass('d-none');
});

$('button.btn-cancel').on('click', function() {
     countPopup --;
     if (countPopup <= 1) $('button.btn-back').addClass('d-none');
});

It is simple and easy to understand, without touching the existing JavaScript code - so less likelihood to break the currently working logic, though old-school style. It worked fine until I found that the previous developer of the JavaScript file dynamically changed one other button to a Cancel button by changing the button text. Oopsie!

Alternatives

It is easy to be tempted to add the dynamically worded Cancel button into the jQuery object to decrease the countPopup variable. It may work, it may not work, I did not try it, so I don't know. But I know it is not a good way, it is not safe.

I need focus on the .modal-body-1 popup itself, when it is visible, hide the Back button, otherwise show the button. So I used window.setInterval to make a watcher to detect the popup visibility as shown below:

JavaScript
function showBtnBack() {
     $('.modal-body-1').each(function () {
         if ($(this).is(':visible')) {
             $('button.btn-back').addClass('d-none');
         } else {
             $('button.btn-back').removeClass('d-none');
         }
     });
}

var timer;
$('#contact-modal').on('show.bs.modal', function () {
    timer = setInterval(showBtnBack, 10);
      ...... (existing code)
}).on('hide.bs.modal', function (e) {
    clearInterval(timer);
    ...... (existing code)
});

Actually, it is a polling. It works fine, no matter what else happens.

Another polling seems better by creating a custom event:

JavaScript
$('div.modal-body-1').on('visible', function () {
     var $el = $(this);
     timer = setInterval(function () {
         if ($el.is(':visible')) {
             $('button.btn-back').addClass('d-none');
         } else {
             $('button.btn-back').removeClass('d-none');
         }
     }, 10);
 }).trigger('visible');

We can also override jQuery .show() function:

JavaScript
(function ($) {
  var originalShowFn = $.fn.show;
  $.fn.show = function () {
     $(this).each(function (i, ele) {
         if ($(ele).hasClass('modal-body-1')) {
             $('button.btn-back').addClass('d-none');
             return false;
         }
     });
     originalShowFn.apply(this, arguments);
     return $(this);
  };
}) (jQuery);

And change $(".modal-body-1").show() everywhere to:

JavaScript
$(".modal-body-1").show(0, function () {
  $('button.btn-back').addClass('d-none');
});

or, by using PlainObject options, to:

JavaScript
$(".modal-body-1").show({
  duration: 0,
  complete: function () {
     $('button.btn-back').addClass('d-none');
  }
});

Obviously, it needs too many modifications to the existing JavaScript codes.

And there are some jQuery plugins to use, such as AttrChange and Watch.

For modern browsers, we can use MutationObserver, which must be the best solution in this case for change detection:

JavaScript
new MutationObserver(function (mutations) {
     mutations.forEach(function (m) {
         if (m.target.style.display == 'none') {
             $('button.btn-back').removeClass('d-none');
         } else {
             $('button.btn-back').addClass('d-none');
         }
     });
 }).observe($('div.modal-body-1')[0], {
     attributes: true,
     attributeFilter: ['style']
 });

MutationObserver has some browser limits. Browsers that support it are:

Image 2

The good news is that we can get a polyfil to MutationObserver for slightly older browsers. Please be informed that both AttrChange and Watch jQuery plugins use MutationObserver internally.

What Else

If it is an Angular, React or Vue project, it is easy to make use of their change detection mechanism to get a simple fix. Framework is great.

Points of Interest

Delving into even simple stuff could open up a new world. Though new technology makes life easier, old ones may still be good to address problems.

History

  • 18th May, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)