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:
It is in a Bootstrap modal popup. There are 7 modal body div
s 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:
<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>
<!--
<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:
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:
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:
$('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:
(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:
$(".modal-body-1").show(0, function () {
$('button.btn-back').addClass('d-none');
});
or, by using PlainObject
options, to:
$(".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:
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:
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