Responsive Design: Don't Use the Same JavaScript Function Twice

Empty string passed to getElementByID()? Why you can't just duplicate javascript functions for mobile and desktop elements.
browser console showing empty string error
Arg!

I've got a simple JavaScript function that relies on getElementById(); for some reason, even though the element ID is 100% kosher (it exists), when I run the function my browser console gives me an error: "Empty string passed to getElementById()." Both the problem and the solution here are pretty simple. Before we get to the juicy bits, though, let's rewind and look at the use case.

Background

Some devs go to school; others come to development, look at the languages, and have to rely on sheer will to power in order to reach the end of a day (... nod to Nietszche and my college years in Philosophy). I'm a pretty firm believer that you don't actually need a degree in computer science to be a good dev; what you need is a keen eye for analysis and application of logic. PHP, JavaScript/jQuery and all the other languages/syntaxes that comprise the internet are systems of logic. If you can figure out the basic logic at play, the rest is just a combination of fresh coffee and Stack Overflow.

Humans, however, are highly illogical. Sometimes we just do stupid stuff--like recycling code without really paying attention to the code or its function. So here I am on a weekday afternoon, second cup of coffee in my hand, tasked with (re)creating a notification block on my clients site. They feel like the block is important enough to be first thing users see on the site (first item in Drupal's block regions), but not important enough to sit there 24/7; they want users to be able to close the block and not see it again for another 24 hours. This isn't the first time we've used blocks like this; we've done it a couple times in the past. This means I actually have an old custom block sitting disabled in the Drupal admin that I can just redress and reuse--always a win for the developer!

Here are the two key points for this post:

  1. The block will use a cookie to ensure it isn't shown again for 24 hours after a user closes or sees it. The function to close the block will use getElementById() to target the parent div of the block.
  2. The block will reside in different block regions for desktop and mobile; I'll actually be working with two different blocks--one for desktop and one for mobile.

Before we get to the problem and solution, let's look at my Desktop block--the HTML and JavaScript. I'll go with some simplified HTML here since the styling is irrelevant:

<section id="block-desktopnotification">
  <div>
    <p>This is an important notification! Thanks for taking notice!</p>
    <div class="closeblock"><a href="#" onclick="closeBlock()">CLOSE</a></div>
  </div>
</section>

There--nothing too complicated: I've got a div with a link in it and I'll use an onclick attribute to allow users to close the block if they'd like to. I'll also check to see if the user has a cookie; if they do, I'll use the same function to close the block and hide it from them; if they don't have the cookie, I'll set one and leave the block visible. This way, they'll only see the block one time before the cookie expires.

Let's have a look at the JavaScript. I'll wrap this in <script> tags and place it in the actual notification block above:


//TRIGGER TO CLOSE
function closeBlock() {
    document.getElementById("block-desktopnotification").style.display = "none";
}
//IF COOKIE SET, CLOSE BLOCK; IF NOT SET COOKIE    
if (document.cookie.indexOf("notified=") >= 0) {
    closeBlock();
} else {
    //CREATE A DATE
    date = new Date();
    //GET THE CURRENT TIME AND ADD ONE DAY
    date.setTime(date.getTime() + 1*24*60*60*1000); //1 DAYS
    //USE THAT VALUE TO ESTABLISH AN EXPIRATION VARIABLE CALLED done
    var done = date.toUTCString();
    //SET THE COOKIE 
    document.cookie = "notified=yes; secure; expires=" + done + "; path=/";
}

That's not too complicated either. The first function, closeBlock(), queries the DOM for an element with the ID of block-desktopnotification and simply sets the style to display:none; when run. This function is primarily used to allow users to close the block. I reference the function in the onclick attribute in my HTML so that when users click the "CLOSE" link the block closes. The second bit in this script is an if statement. The logic is pretty straightforward: if our cookie has already been set, run the closeBlock() function; if not, set the cookie.

Take it to testing and you'll see that this code works fine. It's, to quote a former president, "perfect". 

Quick side note: don't use secure in your cookie unless your site loads in HTTPS--i.e.,  document.cookie = "notified=yes; secure; expires=" + done + "; path=/"; It looks like this will be a requirement for cookie support in browsers moving forward, though. I'm gonna leave the whole discussion of cookie consent out of this as well...

Great: On to the Mobile Block!

Here's where it's tempting to get lazy. Now that we've got a functioning block for desktop, why not just copy and paste the whole thing into a separate block for mobile? A superficial analysis would suggest that in order to get the same thing working for a mobile block, all you'd need to do is update the getElementById() function to target the ID of the mobile block. You'd be correct, but you wouldn't be finished.

The Problem

See, it's tempting, as front end developers, to view Desktop and Mobile as two different realms: how many times do you talk to a client and refer to "the mobile site"--as if it were wholly separate from Desktop? Yes, there are some creative situations in which Mobile can be independent, but for most people running responsive themes or frameworks like Bootstrap, language like "the mobile site" isn't really helpful. Your clients don't need to concern themselves with these kinds of semantics, but it's something you can't forget as a developer.

What do you think, then, would happen if you define a single function two different ways on the same page?

One of them will break. (Likely, whichever is lower in the DOM)

This is exactly how I wound up with an empty string error for getElementById(). See, I copy/pasted my desktop block into my mobile block; I knew my function needed to target a different element ID for mobile, and updated accordingly, but didn't do anything beyond that. This meant I had two different definitions of the same function in two different places. It basically looked like this:


//IN MY DESKTOP BLOCK I HAD THIS
function closeBlock() {
    document.getElementById("block-desktopnotification").style.display = "none";
}

//IN MY MOBILE BLOCK I HAD THIS
function closeBlock() {
    document.getElementById("block-mobilenotification").style.display = "none";
}

I would sit there clicking the "CLOSE" link on my mobile block and nothing would happen: closeBlock() would do nothing.

That's actually incorrect; closeBlock() was working fine, it was just closing the Desktop block instead of the Mobile block. That's because, in my case, the desktop block existed higher up in the DOM; the definition for the closeBlock() function in my Desktop block took precedence. Obviously, since I was viewing "the mobile site" the change in the desktop block went entirely unnoticed.

This is one of those moments when you just feel stupid.

Solutions

I suppose one way of dealing with this would be to simply combine the code; you really only need it in once place. You could, for example, target both blocks from within a single closeBlock() function--like this:


//CLOSE BOTH DESKTOP AND MOBILE
function closeBlock() {
    document.getElementById("block-desktopnotification").style.display = "none";
    document.getElementById("block-mobilenotification").style.display = "none";
}

You might, however, assume that few, if any, people ever view both desktop and mobile from the same browser/device. Maybe you'd like them to see the block regardless of which device they view from. In this case, it's probably best to serve a mobile cookie and a desktop cookie; it's would also be best, then, to change the name of the function for mobile. Here's how that might look:


//TRIGGER TO CLOSE DESKTOP
function closeBlock() {
    document.getElementById("block-desktopnotification").style.display = "none";
}
//IF COOKIE SET, CLOSE BLOCK; IF NOT SET COOKIE    
if (document.cookie.indexOf("notified=") >= 0) {
    closeBlock();
} else {
    date = new Date();
    date.setTime(date.getTime() + 1*24*60*60*1000); //1 DAYS
    var done = date.toUTCString();
    document.cookie = "notified=yes; secure; expires=" + done + "; path=/";
}

//TRIGGER TO CLOSE MOBILE
function closeMobile() {
    document.getElementById("block-mobilenotification").style.display = "none";
}
//IF COOKIE SET, CLOSE BLOCK; IF NOT SET COOKIE    
if (document.cookie.indexOf("mobilenotified=") >= 0) {
    closeMobile();
} else {
    date = new Date();
    date.setTime(date.getTime() + 1*24*60*60*1000); //1 DAYS
    var done = date.toUTCString();
    document.cookie = "mobilenotified=yes; secure; expires=" + done + "; path=/";
}

I could put these in the same place, or I could put them in their respective Desktop and Mobile blocks; the important part is that the functions and cookies are unique; so long as your onclick attributes reference the appropriate function, you're good to go.

I'll leave it at that. Again, this isn't a particularly complicated topic. It makes sense at a pretty fundamental level. The real issue revolves around how we sometimes view our websites. It's easy to divorce Mobile from Desktop. This one didn't take me hours or days to figure out; it did, however, leave me feeling pretty dumb after I realized my mistake. The salt in my wound was that this isn't even the first time I've made this mistake with this very same kind of block.  To me, that indicates it's worthy of a blog post. Hopefully it has use to others out there.