percolate lms logo

The development blog of the Percolate LMS

Tags as an alternative to checkboxes

2018-06-18

It was pretty obvious that we needed to add a checkbox to our interface.

I had just created the printable certificates feature in Percolate and we needed the ability to turn them on and off (not every content item will need a certificate). Sounds like a job for a checkbox, right?

But I was loath to add one.

Checkboxes are surprisingly painful

Why? What’s wrong with an innocent little checkbox?

First, getting the right wording for a checkbox can be surprisingly difficult. Our example here, simply labeled "Print certificate" doesn’t seem too bad at first. But there are complications, starting with the fact that "print" can be a verb!

Unambiguous wording

Can we make it completely clear what will happen when we click this checkbox?

That one is pretty unambiguous, but it’s also incredibly verbose and completely weighs down our interface for this one feature.

At their worst, checkboxes can employ negatives and even double negatives. For example:



Sure, the last one is extreme, but these things do appear out "in the wild" in user interfaces all the time. As Raymond Chen says, don’t require your users to have a degree in philosophy.

The checkbox element itself is a little weird

My other reasons for wanting to avoid checkboxes are (more) selfish. For one thing, they’re fairly miserable to work with. In HTML, checkboxes have a checked state which can be activated by adding a checked attribute:

<label>
  <input type="checkbox" checked> This one is checked
</label>

A checked checkbox will pass the key/value pair of the name attribute (key) equal to the value attribute. An example makes this relationship clearer:

<input type="checkbox" name="animal" value="snake">

   Has key/value of: animal=snake

Wouldn’t it make more sense to just have a true or false value? Yeah, it would. But if you have more than one checkbox with the same name, it will pass both values:

<label>
  <input type="checkbox" name="option1" value="special"> Make it special
</label>
<label>
  <input type="checkbox" name="option1" value="special2"> Make it more special
</label>

If submitted using an HTML <form>, these checkboxes would be sent to the server as:

option1=special&option1=special2

This is often presented to the developer as an array once it’s been handled by a web framework.

It can be handy in circumstances where you wish to collect a list of options from the user, but in my opinion, that’s a bit of an edge case.

The real icing on the cake that an unchecked checkbox is not sent at all, which can be awkward to deal with. This behavior only makes sense in a "list" context which, as I said, seems like a bit of an edge case. Mind you, all of this is stuff you learn early on in web development and we seldom give it a second thought. But that’s how it is.

Percolate’s user interface is a single-page application and as such, we can poll checkbox elements to see if they’re checked or not, which better fits the boolean "on/off" mental model we’re likely to be employing with this element.

In raw JavaScript, this might look like:

var is_checked = document.getElementById('my-checkbox').checked;

But the disconnect between this typical usage and the way html <form> handling is done is, I believe, unfortunate.

Of course, that’s just the collecting of the value.

Everything that is done to collect the checkbox value will have to be done in reverse to present the value for the user to edit later. That is, we have to present this value by either adding the checked HTML attribute when rendering, or by setting the checked property via JavaScript.

Plenty of frameworks will abstract away some of this inelegance, but it’s always there, lurking.

Another checkbox, another database field

My final argument against presenting options like this as checkboxes is that they’re no fun to store and retrieve. This probably sounds bizarre: what could be simpler than storing a boolean value? It’s just true or false, right?

Well, how are we going to store this value?

The obvious answer is to add a column to your table.

Let’s say we’re setting options for "things" and we want to add an option for, say, option1. So we add a column and we’re all set:

The "thing" table:
+----------+------+---------+
| thing_id | name | option1 |
+----------+------+---------+
|   837837 | Foo  | false   |
|   389271 | Bar  | false   |
|   551331 | Baz  | true    |
+----------+------+---------+

(And by the way, let’s just forget about the fact that there is no one boolean datatype that is supported by all relational database systems, so you may be storing them as BOOL or BOOLEAN, but might also be storing them as INTEGER or TINYINT or BIT or even CHAR(1).)

But here’s the thing: once you open the floodgate of adding columns to your tables, you may find that they start to accumulate over the years. (option1, option2, option3…​) Do you want to have to add columns to your production database every time a new option gets added to your thing type?

The sad future "thing" table:
+----------+------+---------+---------+---------+-----+
| thing_id | name | option1 | option2 | option3 | ... |
+----------+------+---------+---------+---------+-----+
|   837837 | Foo  | false   | false   | true    |     |
|   389271 | Bar  | false   | true    | false   |     |
|   551331 | Baz  | true    | false   | true    |     |
+----------+------+---------+---------+---------+-----+

No, nobody wants that. So the "right" way to do this in a relational database is to normalize this data so that the columns do not grow as new options are added. This will require one or more additional tables to store the relationship:

Normalized "thing":
+----------+------+
| thing_id | name |
+----------+------+
|   837837 | Foo  |
|   389271 | Bar  |
|   551331 | Baz  |
+----------+------+

Normalized "thing_option":
+----------+---------+-------+
| thing_id | option  | value |
+----------+---------+-------+
|   837837 | option1 | true  |
|   389271 | option2 | false |
|   551331 | option2 | false |
|      ... | ...     | ...   |
+----------+---------+-------+

Now you can add as many options as you want and never have to modify your database again. But at a cost of complexity. Retrieving this information will require table joining (or subqueries) and guaranteeing default values and such will require more programming.

A slightly more exotic (but arguably more correct from the standpoint of database normalization) way to store our options would be to have a single table per option:

Normalized "thing":
+----------+------+
| thing_id | name |
+----------+------+
|   837837 | Foo  |
|   389271 | Bar  |
|   551331 | Baz  |
+----------+------+

Normalized "thing_option1":
+----------+
| thing_id |
+----------+
|   837837 |
|   551331 |
|      ... |
+----------+

With this model, the mere presence of the thing_id key in the thing_option1 table indicates a true value for Option 1. Rather than setting true and false values, you can INSERT or DELETE. The advantage of this method is that there is no "missing" or "NULL" possibility in this table - an absence is, at worst, merely a false.

The obvious downside to this model is the need to create a completely separate table for each additional option! Yuck!

(There is yet another alternative to storing our options in separate relational tables. You can store these multiple values in a single column using a data structure provided by the database itself or creating your own ad hoc format(!). Needless to say, the pros and cons of this type of approach is way outside the scope of this article.)

All of these methods have their upsides and downsides. The pragmatic programmer takes both present and future pain into account when choosing.

Tags to the rescue

So while the checkbox is the "obvious" interface element to have, I didn’t want to rush into adding one. Were there any alternatives?

With the Percolate LMS, tags are central to both user and content organization. All things being equal, the answer to almost any question about Percolate is answered with "tags". It’s safe to say that we and our end users have already paid for the cognitive load imposed by tags, so in a way, using them is "free".

So how about this certificate problem: can tags get us out of the need for a checkbox user interface element and the boolean database value?

Percolate has a concept of "system tags". These are tags prepended with a $ character and they have special meaning to the LMS itself.

Our solution to making certificates optional was to add a new system called called $certificate. If you add $certificate to any piece of content, that content has a printable certificate made available.

The neat thing about system tags is that they otherwise function as perfectly normal tags. Your can filter and search with them, assign by them, etc. So by making the certificate a tag-specified option, we get what I refer to as a "free report". Want to see all of the content with certificates? Just filter by the $certificate system tag and there you go:

filtering by certificate tag
Figure 1. Content filtered by the $certificate tag

Some folks like to say that if all you have is a hammer, everything looks like a nail and maybe there is a danger in trying to shoehorn everything into this tag concept.

But there is also serious power in uniform interfaces. At their best, they allow the user to invent new ways to use them - creative and clever ways to solve problems we never even imagined. I’m not sure whether or not we’re quite there yet with Percolate, but that’s the dream.

The user has already learned the interface

Since we already have the concept of tags in Percolate, administrators will have already learned how to use them. Explaining that "certificates are enabled with a tag" doesn’t introduce any new interface features, nor do we have to take screenshots or write out long descriptions explaining where to find a checkbox. (Truly, is there anything more annoying than knowing a feature exists, but having to hunt through menus to find it?)

The only user interface which is not confusing is the one that was never needed in the first place.

By reusing tags as much as possible, we hope to give the user both familiarity and power. Ideally, they give the user an intuitive grasp of the interface (the sense that, "I’ve seen these before. I know how these work.")

One of the chief design goals of Percolate 2.0 was to expose all of its functionality through its API. We eat our own dogfood with the default user interface we provide. Our idea of "user" encompases not only the learner and the LMS administrator, but also the developer who adds functionality to the LMS through the API. Here too, there can be no question that a smaller API surface area is far easier to quickly comprehend than a sprawling one with many different concepts.

Less novel interfaces equals less bugs

As every developer knows, the only only good code is the unwritten code. Code is our enemy and we should strive to have as little of it as possible. As should hopefully be clear after reading why "checkboxes are a pain" above, adding any new kind of interface element brings with it a data structure and a whole chain of code to capture, create, read, render, update, and delete the data.

We can’t always avoid different types of user interfaces (and accompanying code), but we can strive to creatively think around them when possible.

If the result is something that is easier to explain and affords more power…​well, then that’s just awesome.