Write Less JavaScript

How many lines of JavaScript code were used to create the following interface?

At Mystery Science, we recently released this feature to capture feedback after a teacher has taught a lesson. There are four possible screens or states:

  1. Prompt (did you teach?)
  2. Rating (stars)
  3. Comments (open-ended feedback)
  4. Done

Each state (except the last one) prompts and subsequently saves user input. The server knows I taught a lesson as soon as I click “yes.” The star rating and comments work the same way — all responses are submitted via XHR. How little JS code can we write to get this feature working?

The first trick to writing less JS is to find the right abstractions. Let’s explore the two key abstractions here. When a teacher submits a response, JS is advancing the interface’s state. This can be accomplished in any number of ways but at the end of the day some elements are being shown (display: inherit1) and others are being hidden (display: none). JS also has to send the response to the server, which in jQuery-terms eventually means a call to $.ajax().

Let’s wire this up:

// Let's assume this feature lives inside <div class="lesson-feedback">.
var container = $('.lesson-feedback');

// First, let's wire up the UI:
container.find('.yes-control').click(enterRatingState);
container.find('.rating-control').click(enterCommentsState);
container.find('.no-control, .submit-comments-control, .skip-comments-control')
  .click(enterDoneState);

// Now, let's wire up the response submissions:
container.find('form').submit(submitViaXHR);

This is a great start! This code is readable and there’s not much of it. But we’ve introduced some event handlers like enterRatingState() that are not defined. Presumably, these functions will reach into the container and show and hide their respective elements. But given that each state has many elements, how can we do this without introducing a bunch of JS? Let’s introduce some CSS to do the heavy-lifting. This is the second trick to writing less JS: defer view logic to CSS whenever possible.

.lesson-feedback.state-rating .rating,
.lesson-feedback.state-rating .visible-during-rating {
  display: inherit;
}
.lesson-feedback.state-rating .prompt,
.lesson-feedback.state-rating .comments,
.lesson-feedback.state-rating .done,
.lesson-feedback.state-rating .hidden-during-rating {
  display: none;
}

Let’s unpack this. Obviously, .lesson-feedback is the container. We’ll modify the container with .state-* classes to enter and exit the four states. This means there will also be .state-prompt, .state-comments, and .state-done. Each state needs blocks like the ones above. I’ve also included .visible-during-* and .hidden-during-* helper classes to control elements that span states (like the header and image). It may seem like a lot of CSS but once it’s there you shouldn’t need to change it unless you add or remove a state. Some of the repetition can also be reduced with a preprocessor like LESS or SASS.

Now, enterRatingState() and the other state-related functions are really lightweight:

// You'll need a function like this for each state:
function enterRatingState() {
  enterState('rating');
}

function enterState(state) {
  container.removeClass('state-prompt state-rating state-comments state-done');
  container.addClass('state-' + state);
}

That’s all we need to drive the interface; looking good! Now, let’s look at data submission. In the first code example, I introduced submitViaXHR(). Let’s define that now.

function submitViaXHR(event) {
  event.preventDefault()

  var form = $(event.target);

  $.ajax(form.attr('action'), {
    type: form.attr('method'),
    data: form.serialize()
  }
}

Remember, all the forms have submitViaXHR() bound as an event handler on submit. Instead of submitting to the server with a traditional page refresh, jQuery steps in and submits the data via XHR instead — this is nothing new. The real elegance here is that they all submit the same way. There are not three different handlers for the prompt, rating, and comments forms. The third trick to writing less JS is to keep data in HTML. Here’s an example of the HTML for the first state, prompt:

<div class="lesson-feedback state-prompt">
  <div class="prompt">
    <div class="answer">
      <form action="/viewings" method="PUT" style="display: inline;">
        <input type="hidden" name="viewing[actually_taught]" value="true">
        <input type="submit" value="Yes" class="yes-control button button-green">
      </form>

      <form action="/viewings" method="PUT" style="display: inline;">
        <input type="hidden" name="viewing[actually_taught]" value="false">
        <input type="submit" value="No" class="no-control button button-red">
      </div>
    </div>
  </div>

  <!-- And so on for the other states... -->
</div>

There are two really lightweight forms with hidden values that submit to the server. That’s all we need. The destination of the request (/viewings), the HTTP verb (PUT), and the request data ({ viewing: { actually_taught: true} }) are all stored in the markup and that’s a good thing.

So let’s recap. To write less JavaScript:

  1. Find the right abstractions.
  2. Defer view logic to CSS whenever possible.
  3. Keep data in the HTML.

1: inherit is a better choice than block for visible elements. This approach will allow inline elements to preserve their inline display type.

Thanks for reading! I'm Avand.

I’ve been working on the web for over a decade and am passionate about building great products.

My last job was with Airbnb, where I focused on internal products that helped teams measure the quality of the software they were building. I also built internal tools for employees to stay more connected, especially after the COVID-19 pandemic. Before that, I was lead engineer at Mystery Science, the #1 way in which science is taught in U.S. elementary school classroms. For a while, I also taught with General Assembly, teaching aspiring developers the basics of front-end web development.

I was born in Boston, grew up in Salt Lake City, and spent many years living in Chicago. Now, I call San Francisco my home and Mariposa my home away from home.

I enjoy the great outdoors and absolutely love music and dance. Cars have been an lifelong obsession of mine, especially vintage BMWs and Volkswagens. I’m the proud owner of a 2002 E-250 Sportsmobile van, and he and I have enjoyed many trips to beautiful and remote parts of the West Coast to create good vibes.

What can I do for you?

Read my other posts or get in touch: