Ten years ago, I wrote a piece of JS code to add a button to toggle the visibility of all R code blocks on a HTML page, which partially implemented the code-folding feature. The original code was this:
function toggle_R() {
var x = document.getElementsByClassName('r');
if (x.length == 0) return;
function toggle_vis(o) {
var d = o.style.display;
o.style.display = (d == 'block' || d == '') ? 'none':'block';
}
for (i = 0; i < x.length; i++) {
var y = x[i];
if (y.tagName.toLowerCase() === 'pre') toggle_vis(y);
}
}
document.write('<button type="button" onclick="toggle_R();" style="position: absolute; top: 0; right: 0;">Hide/Show Source</button>')
When I was thinking about code-folding again a few weeks ago, I dug out the above code, and felt it was worth commenting after a decade. The rewritten version of the above code can be found at the end of this post.
Bye, jQuery! Hi, vanilla JS!
As a JS amateur, my biggest change over these years was that I stopped using
jQuery. Because I was an amateur, I thought jQuery was the “right” way to write
JS. A lot of code that I saw was using the magical $()
, which made me believe
the only way to select elements on the page was $()
. I was aware of methods
like document.getElementById()
and document.getElementByClassName()
, but
sometimes I wanted a more flexible method to select elements (not just by their
IDs or class names). You can see in the above code that I first try to get all
elements by the class name r
:
x = document.getElementsByClassName('r');
and then check if the tag name of an element is pre
:
for (i = 0; ...) {
if (x[i].tagName.toLowerCase() === 'pre') ...
}
That was awkward.
It was quite a few years later that I discovered the document.querySelector()
and document.querySelectorAll()
methods in vanilla JS. At that moment, I could
not believe it was that simple. You can use CSS selectors to select elements!
Want to select <pre>
with the class r
?
document.querySelectorAll('pre.r');
That’s it. So straightforward.
In retrospect, I’d blame it on the Internet Explorer (IE) for the lack of support, especially IE6, which I had used for several years. Ironically, I remember how excited I was when I first saw IE6 came out: it looked pretty on the pretty Windows XP! Today I still think XP was pretty, but IE? IE wasted my life. Again, ironically I remember how excited I was when I figured out how to support file uploads for Shiny in IE8/9, but why did I have to spend time on that in the first place when all other web browsers did not require this special care?
Anyway, it is a relief that IE has pretty much died now.
To be fair, jQuery is a nice abstraction and has many merits. The problem is that they are rarely what I need now, so I’d rather not take this dependency (otherwise I will have to pay attention to its updates). Vanilla JS is often good enough for me. If you miss the terse dollar sign, I just learned yesterday (!) that you could create one by yourself, e.g.,
const $ = document.querySelectorAll.bind(document);
$('pre.r');
It is not equivalent to jQuery’s $
, but can be a nice and useful shorthand
anyway. Chrome has done something similar to this in the Developer Tools ($()
is basically document.querySelector()
, and $$()
is
document.querySelectorAll()
).
BTW, it took me a few more years to realize that the querySelector()
method
could be used on any DOM elements, not just document
. I have been this slow in
learning.
Bye, for
loops! Hi, .forEach()
!
I was too used to writing for
loops in JS, and there had been a pattern like
this:
for (i = 0; i < x.length; i++) {
y = x[i];
// more work on y
}
There is no need to use a loop or create y
(or the looping index i
) when x
is an array and you want to deal with its elements one by one. The forEach()
method is much cleaner.
x.forEach(y => {
});
When x
is not an array but an array-like object such as the returned value of
getElementsByTagName()
, you can convert it to an array first using the ...
operator, e.g.,
[...document.getElementsByTagName('pre')].forEach(() => {});
If you read the code in the beginning of this post carefully, you may notice
that I actually created a global variable i
inadvertently: I should have used
for (var i = 0)
instead of for (i = 0)
. Using the forEach()
method can
avoid this problem.
To be clear, I still use for
loops nowadays, but just not that often. Loops
have their advantages, e.g., you
can break
a loop early when necessary.
Bye, function() {}
! Hi, () => {}
!
I use =>
to create functions now because it is shorter and still easy enough
for me to read. This is purely a cosmetic preference.
I still use the function
keyword when the function is meant to be called in
several places. I prefer the =>
shorthand when I want to create a function to
be used once somewhere (e.g., an anonymous function to be used as an event
handler).
Bye, document.write()
! Hi, .insertAdjacentHTML()
!
I do not know why document.write()
was prevalent when I started learning JS.
Perhaps I learned through some fake tutorials. To add some HTML code to the DOM,
I’d use the insertAdjacentHTML()
method now. One problem with
document.write()
is that if you run it in the JS console of the browser, it
can overwrite the full HTML code of the page, and you certainly do not wish to
destroy the whole page.
Normally I’d avoid inserting raw HTML code, but sometimes I’m just too lazy to
document.createElement()
and append it with the .before()
or .after()
methods.
No global variables if possible
Creating global variables means potential clashes and pollution of the global
namespace. In my original code, I create a global function toggle_R()
, because
I wanted to call it in the onclick
event of the button. Now I’d not write the
event handler to the onclick
attribute of the <button>
, but select the
button in JS and attach the event to it instead. The latter way will not create
a global variable.
Simplifying a conditional expression
Toggling the visibility of an element can be achieved by changing its display
property in CSS. For a code block, display: none
means to hide it, and
display: block
means to show it. Originally my code was like this:
el.style.display = (el.style.display === 'block' || el.style.display === '') ? 'none' : 'block';
Now I’d write:
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
They are equivalent based on the assumption that display == ''
means the block
is shown, which is not strictly true but true most of time. If we compare the
display
value against none
first, we can get shorter and simpler code than
when we compare display
against block
or an empty string.
From 15 lines of code to 8: shorter, safer, and easier to reason about
Below is the “modernized” version of the code:
(d => {
d.insertAdjacentHTML('beforeend','<button style="position:absolute;top:0;right:0;z-index:2;">Toggle Source</button>');
d.lastElementChild.onclick = () => {
d.querySelectorAll('pre.r').forEach(el => {
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
});
};
})(document.body);
The construct (d => {})(document.body)
may feel like ninja code, but it is
just because I’m lazy to write:
(() => {
const d = document.body;
})();
We added the button the last of the document, so d.lastElementChild
is the
button, and we attached a click
event to it. If you run the code, you should
see a button at the top-right of the page. If you click on it, it should toggle
R code blocks on the page (if there are any).
I guess JS experts may find it funny that I used to think I could not live
without jQuery, and my discovery of querySelectorAll()
changed my way to use
and write JavaScript. I find it funny, too. In programming, abstraction
libraries are often nice, but sometimes if we look closer, we may find that all
we need is actually a tiny core feature that has existed for a long time in the
base, and for some reason, we have missed it for years.