Recently, I’ve been playing around with _hyperscript. It’s from the same folks that made HTMX, so you know it’ll be an… interesting project. I used it to build typehand which is a tiny little typing speed measurer inspired by typings.gg. If you want to see an explanation of the code, skip to the typehand section.

Personal Tangent

When I first started doing web development around 2012, I would manually link to scripts (usually jQuery) and CSS files in the head of my HTML before writing code. I had heard about this cool thing called “npm” and “node” but trying to install it on my Windows laptop was a hassle. Once I did manage to somehow install it, it made this folder called node_modules that was impossible to delete. Eventually, WSL came along and I was able to successfully install and use the npm ecosystem.

After a couple of years though, the npm ecosystem no longer felt like it was helping me build projects faster. I’d go to make a tiny website that would just generate a random word or submit a form and be pulling in VueJS out of sheer habit (along with its dependencies). Build times were slow as well on my hardware, usually around 10 seconds to hot reload and closer to a minute to build. When I went to npm install something, it became mentally exhausting to watch something pull in hundreds and hundreds of Javascript and CSS files. Web dev just didn’t feel as fun as it had been when I started. When you have a hammer, everything starts to look like a Single Page Application and I wanted out.

Don’t get me wrong - SPAs have their place in the world. There are quite a few web applications I interact with and depend on to do work, which I can’t imagine using if they didn’t have some of the features of an SPA. It was around this point that I saw HTMX, and tried using it in a few small projects. It’s very powerful and could probably replace needing to write Javascript for many, many websites.

What about websites that actually need a bit of JS though? Or those that don’t need a server running backend logic?

That’s what hyperscript aims to solve.

Hyperscript

hyperscript is a language that works especially well for those little interactions that need a bit of JS to work. I like to think of it as jQuery if it were it’s own language, or as a language dedicated to working with the DOM.

Their website states

hyperscript is an easy and approachable language designed for modern front-end web development

I’ve found this to be a pretty accurate statement of what one can do with it.

Well hold on, I’ve seen languages try to replace JS before. How is this different from Typescript, Flow, and Coffeescript?

I’ve seen some of the hyperscript developers describe their approach as “intentionally unscaleable”. Those languages above are designed to solve the problem of writing correct Javascript quickly. I’m going to generalize and say that they’re also designed to write business logic, aka code that runs via Node on a server. hyperscript doesn’t do that. Writing business logic in Hyperscript is a lot more painful than those languages (including JS), but working with the DOM is a lot, lot easier.

Enough talk - here’s a quick example to show an element after clicking a button taken from their website. In other words, on click show #show-target.

JS

<button onclick="document.getElementById('show-target-1').style.display = 'block'">
  Show Element
</button>
<div style="display: none" id="show-target-1">
  Hidden Element
</div>

jQuery

<script>
$(function(){
  $("#showBtn").click(function(){
    $("#show-target-2").show();
  });
});
</script>
<button id="showBtn">
  Show Element
</button>
<div style="display: none" id="show-target-2">
  Hidden Element
</div>

hyperscript

<button _="on click show #show-target-3">
  Show Element
</button>
<div style="display: none" id="show-target-3">
  Hidden Element
</div>

I also really like this demo that the developers posted on Twitter, it shows just how powerful it can be compared to what you’d need to write out in Javascript.

typehand

As usual, when I want to try out some new framework, language, or stack, I build a small project to validate its claims. I figured a typing test website would be large enough project to attempt doing, without it taking too long. I won’t be going through all of the code/HTML, just the bits with hyperscript. I would recommend going to typehand and right clicking to view the source if you want to follow along (never thought that would be useful in 2021). I’ve also put the source in a Github gist. This code uses the development build of hyperscript at the time of writing, not the latest stable release.

Requirements

  1. Randomly generate words to type from a wordlist (in this case, generously taken from typings.gg).
  2. Allow a user to select how many words to type [10, 25, 50, 100, 250]
  3. Have the word that the user needs to type highlighted. If the user mistypes the word, mark it as incorrect. If the user types the word correctly, mark it as green.
  4. As the user types the word, the moment they make a mistake on that word, make the typing field change color to tell them that somehow.
  5. Once the user has typed the last word, calculate their accuracy and words per minute (WPM). Display those statistics.

Code Explanation

I’ll be jumping around a bit. Thankfully the code is so short that it shouldn’t be too difficult for anyone reading this to following along.

First, loading the wordlist.

init fetch words.json as json 
then set $allwords to it
then trigger reset(count: 50) on #words

The first line here does a fetch request to words.json, which is just a JSON file containing English words in an array. The second bit triggers an event called reset on the element with an id of words. Notice the async transparent nature of hyperscript when we trigger the event after the JSON has loaded - you don’t see any promises and callbacks here, even though internally that’s what is happening.

on reset(count) 
  set $startTime to -1

Let’s jump to the reset event, since we trigger it on load. The first line listens for it being triggered, and it additionally unpacks event.count into the count variable. The next line sets $startTime to -1. The $ in front of startTime means it is a global variable, so any hyperscript can access it. hyperscript does have element scoped variables, but as I was trying to keep things simple I didn’t use them. $startTime is used to hold the (Unix) time that the user starts typing - when we reset the words to be typed however, the user hasn’t typed so we just set it to -1.

  repeat count times 
    append `<code class='yet'>${random in $allwords}</code>` to randwords 
  end
  set my innerHTML to randwords

Apologies for the syntax highlighting - there is a Prism based highlighter for hyperscript here, but my blog doesn’t support it.

Anyway, the next line repeats count times, the parameter passed in as an event. We append a string with interpolation to the variable randwords. hyperscript is pretty loose with variable declarations and some scoping rules to make it easier to write simple code. Note the bit inside of the ${} block. Each iteration, we end up selecting a random word from $allwords, the global variable we filled with words using JSON earlier. We then go ahead and set this HTML string to fill the element’s innerHTML. We’ve fulfilled requirement 1.

  add .light-purple to first .yet
  set #maininput's value to ''
  call #maininput.focus()

Finally, we add the class .light-purple to the first word that is .yet to be typed, fulfilling part of requirement 3. Since this all part of resetting the list of words, we are now ready to let the user start typing. We clear the input field and focus it, so that they can immediately start typing.

on load 
  tell my children add .secondary add [@href=#]
on click from <a/> in me 
  take .contrast from <a/> in me for it 
  trigger reset(count: it.innerText) on #words

You can listen for multiple events in hyperscript, and that’s what we do there. First, when the page loads we go ahead and add some classes and attributes to the children, which are the links we can click to change the number of words to be typed. Note that the next line listens for a click on the parent element. This is simpler than needing to listen to each link for a click.

Once someone clicks on the child <a/> tag, we take the .contrast class from all of them, and add it to the link that was clicked. We then trigger the reset event as before, but using the innerText of the clicked link to fill in the count. With this, we’ve fulfilled requirement 2.

on click trigger reset(count: #words.children.length) on #words

This is the code for the reset button. It is pretty similar to how the links from above work, only we set the count to be the number of children (words) that the element with id words has.

on input[.yet's length is 1 and my value is .yet's innerText] or
keydown(key)[key is ' ' and some .yet] 
  if my value 
    call nextWord(my value) 
  end 
  set my value to '' 
  halt

The events on the input here are pretty dense, so I’ll try my best to explain them. This event handles when to go ahead and write the next word. There are basically two times we want to do this: the user hits space after typing some text, or if the user has typed the last word correctly.

The first event we check for here is checking that second condition - is there one word left to type, and did we type it correctly. If that’s the case, it’ll run the code at the end. Note that we are listening to event 1 or event 2 happening to run this code - earlier we were listening to two different events with two different pieces of code.

The conditions for the second event is that the user pressed the spacebar down, and that there are still words .yet to be typed. So if either of these events are true, we do a few things. First, if there was something typed (the input isn’t blank), we call the writeWord function with the value inside of the input. Regardless of whether there was something in the input, we clear the value (setting it to ‘’) and halt, which stops the event (preventDefault for the JS folks). This stops the space from actually being added to the input, as this is a keydown event which runs before the input has been updated with the value of the key.

on input[$startTime is -1] set $startTime to Date.now()
on input[some .yet] 
  set @aria-invalid to not (first .yet's innerText).startsWith(my value)

These are easier to explain thankfully. The first one just sets $startTime to the current time when the user types (so long as it was just reset, setting its value to -1). The second one handles requirement 4. As long as there are words remaining, it sets whether the field is valid or not based on whether the value of the input starts with the current word we need to type (first of the words yet to be typed). Since the attribute is invalid we need to negate it with the not.

def nextWord(value)
  tell first .yet
    remove .yet .light-purple
    if value is your innerText add .green otherwise add .red end
    if no .yet
      set minutes to (Date.now() - $startTime) / (1000 * 60)
      put (#words.children.length / minutes) as Fixed into #wpm
      put ((.green.length * 100) / #words.children.length) as Fixed into #acc
    otherwise add .light-purple to next .yet end
end

Last stretch here! This is a function in hyperscript. I separated it out to aid in readability, but it technically could have been embedded into the input tag.

The tell statement here basically aliases first .yet to the word you, and it makes some operations affect it by default. Again, first .yet is the word that the user just typed when we end up in this function. We remove the classes yet and light-purple from that word. If the value matched the word itself, we add green, otherwise we mark it as red (requirement 3). The if statement check if there are no .yet left - in other words, no words left to type. If that is the case, we do some math to get the WPM. Notice the accuracy calculation - we just use the DOM to hold this state for us and figure out how many words were red to calculate the percent. With that, we’ve met requirement 5.

If there are words left however, we need to highlight the next word in purple, which is exactly what that last piece of code does.

Conclusion

Personally, this was fun! I can imagine that writing this in normal ES5 would have been a bit of a slog, as I know that the difficult part of that would just be finding all of the DOM manipulation methods and figuring out when I need to iterate a list of elements to add and remove classes.

As far as performance goes, yes hyperscript is slower than Javascript. It’s an interpreted language implemented in Javascript. For the majority of interactions you use it for though, I’d expect performance to not matter all that much - it’s still just doing DOM manipulation the same way Javascript does.

I’m not claiming that hyperscript will replace Javascript, but it is very interesting to use. The strangest thing about this project was that I found it slightly more difficult to write code, but much easier to read my code back. Everything was plain English, and I didn’t have to go digging too much for JS functions that manipulate the DOM. In other words, if you already understand DOM events, you can pick up hyperscript in a few hours and be productive.

Speaking of productivity, I was able to write this simple project in a few hours. It was pretty much my first time using hyperscript to do anything substantial, and I’d say it went pretty well. However, I did find a few bugs and make a few feature requests as I was writing this code. hyperscript is still in beta, though its shaping up nicely. If you’re planning on using this for a larger project, you may want to wait for 1.0 to avoid any sort of breaking changes.