While working on Avris Forms v4.0, I’ve decided to migrate some code from CoffeScript with jQuery to Vanilla JS. And I guess it might be a good idea to share this transition
The code handles the form widget visible in the screenshot above. More specifically: those buttons in the right column. Clicking on the green one adds a new element/row based on a template hidden inside a <script>
tag. Clicking on a red button results in removal of the current row (after asking for a confirmation).
$('body').on 'click', '.form-multiple-add', ->
$form = $(this).parents('.form-multiple')
newIndices = $form.find('[data-index^=new]').map((i, el) -> el.dataset['index'].substr(3)).get()
newIndex = if newIndices.length then Math.max.apply(null, newIndices) + 1 else 0
$template = $($form.find('.form-multiple-add-template').html().replace(/%i%/g, 'new' + newIndex))
$(this).parents('tr').before $template
$template.find(':input:enabled:visible:first').focus()
$('body').on 'click', '.form-multiple-remove', ->
return false if !confirm('Are you sure?'))
$(this).parents('tr').remove()
It was not a part of the library (as the library was supposed to only provide PHP), instead each project included this part on its own. Which led to code duplication (and the worst kind: between projects) and also kinda prevented me from abandoning CoffeeScript in newer projects, even if I wanted to.
And I did want to, because JavaScript has matured as a language over the last years. An additional level of complexity in my projects simply wasn’t necessary anymore. CoffeeScript did a great job of making fontend scripting bearable, but now it’s slowly time for it to get a deserved retirement.
So I’ve decided to integrate the frontend part into the library. Except I shouldn’t be forcing users to use CoffeeScript or jQuery, it should just work out of the box in any project.
Since CoffeeScript compiles into JS (although usually with from some unnecessary returns and similar features), getting rid of it is really simple. After using js2coffee or decaffeinate we would end up with something like this:
$('body').on('click', '.form-multiple-add', function() {
var $form, $template, newIndex, newIndices;
$form = $(this).parents('.form-multiple');
newIndices = $form.find('[data-index^=new]').map(function(i, el) {
return el.dataset['index'].substr(3);
}).get();
newIndex = newIndices.length ? Math.max.apply(null, newIndices) + 1 : 0;
$template = $($form.find('.form-multiple-add-template').html().replace(/%i%/g, 'new' + newIndex));
$(this).parents('tr').before($template);
return $template.find(':input:enabled:visible:first').focus();
});
$('body').on('click', '.form-multiple-remove', function() {
if (!confirm('Are you sure?')) {
return false;
}
return $(this).parents('tr').remove();
});
Removing the dependency on jQuery is more tricky.
There are some nice lists that show you how to replace some jQuery usage with plain JS. Those Javascript versions used to be longer, more complicated and require some browser-specific checks and adjustments. Those times are almost over though (as you can see from the list), so the need for jQuery gets smaller and smaller.
Replacing those calls from the list with Vanilla JS’s counterparts was not enough in my case. I needed two functionalities more: finding an element’s parent by a selector ($el.parents('.foo')
) and binding an event listener to elements matching a selector, even if those elements don’t exist yet at the moment of binding ($('body').on('click', '.foo', => ... )
).
They were surprisingly easy to implement:
var findParent = function(el, selector) {
do {
if (el.matches(selector)) {
return el;
}
el = el.parentNode;
} while (el && el.matches);
};
var on = function (selector, event, handler) {
document.addEventListener(event, function (e) {
var match = findParent(e.target, selector);
if (match) {
var result = handler.apply(match, [e]);
if (result === false) {
e.preventDefault();
e.stopPropagation();
}
}
});
};
With those two helpers I could transform my code to this vanilla JavaScript:
on('.form-multiple-add', 'click', function () {
var form = findParent(this, '.form-multiple');
if (!form) {
return;
}
var newIndices = Array.from(form.querySelectorAll('[data-index^=new]')).map(function (el) {
return el.dataset['index'].substr(3);
});
var newIndex = newIndices.length ? Math.max.apply(null, newIndices) + 1 : 0;
var template = form.querySelector('.form-multiple-add-template').innerHTML.replace(/%i%/g, 'new' + newIndex);
findParent(this, 'tr').insertAdjacentHTML('beforebegin', template);
var firstInput = form.querySelector('[data-index=new'+newIndex+'] :enabled');
if (firstInput) {
firstInput.focus();
}
});
on('.form-multiple-remove', 'click', function () {
if (!confirm('Are you sure?')) {
return false;
}
var row = findParent(this, 'tr');
row.parentNode.removeChild(row);
});
Not bad, is it?