For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):


npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:


joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you do this, we need to (optionally) install one more package, commonmark:


cd app && npm i commonmark

Once this is installed, go ahead and start up your app:


joystick start

After this, your app should be running and we're ready to get started.

Adding some test content

To help contextualize our work, real quick, we're going to add a file with some test content to our app:


export default {
  title: 'How to Get a Dog to Stop Chasing Squirrels',
  content: `
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor purus non enim praesent elementum facilisis leo vel fringilla. Purus faucibus ornare suspendisse sed nisi. Cum sociis natoque penatibus et magnis dis parturient. Suspendisse interdum consectetur libero id faucibus nisl tincidunt. Diam sollicitudin tempor id eu nisl. Erat imperdiet sed euismod nisi porta lorem mollis aliquam ut. Volutpat consequat mauris nunc congue nisi vitae suscipit. Urna cursus eget nunc scelerisque viverra mauris in aliquam sem. Nulla porttitor massa id neque aliquam vestibulum morbi blandit cursus. Diam quam nulla porttitor massa id. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse sed. Ut morbi tincidunt augue interdum velit.
  Et tortor consequat id porta nibh venenatis cras sed felis. Varius sit amet mattis vulputate enim nulla aliquet porttitor lacus. Cras ornare arcu dui vivamus arcu. Consequat ac felis donec et odio pellentesque diam volutpat commodo. Aliquam faucibus purus in massa tempor nec feugiat nisl pretium. Sagittis aliquam malesuada bibendum arcu vitae elementum. Ultricies mi eget mauris pharetra. Venenatis urna cursus eget nunc. Vel pharetra vel turpis nunc eget lorem. Risus pretium quam vulputate dignissim suspendisse in est. At auctor urna nunc id cursus metus aliquam eleifend. Libero enim sed faucibus turpis in eu mi bibendum neque. Nulla aliquet porttitor lacus luctus accumsan tortor. Est ante in nibh mauris cursus mattis. Quam vulputate dignissim suspendisse in est ante.
  Neque convallis a cras semper auctor neque. Dolor magna eget est lorem ipsum dolor sit amet consectetur. Sed faucibus turpis in eu mi. Ullamcorper velit sed ullamcorper morbi. Eu turpis egestas pretium aenean pharetra magna ac placerat vestibulum. Imperdiet proin fermentum leo vel. Velit scelerisque in dictum non consectetur a erat nam at. Sollicitudin nibh sit amet commodo nulla facilisi. Massa sapien faucibus et molestie ac feugiat sed. Amet massa vitae tortor condimentum lacinia quis vel eros donec. Non consectetur a erat nam at.
  Ligula ullamcorper malesuada proin libero nunc consequat interdum. Non tellus orci ac auctor augue mauris. Sem nulla pharetra diam sit amet nisl suscipit adipiscing. Tincidunt ornare massa eget egestas purus. Libero nunc consequat interdum varius sit amet. Nisl nisi scelerisque eu ultrices vitae. Odio ut enim blandit volutpat maecenas volutpat blandit aliquam. Vivamus at augue eget arcu dictum varius duis. Pretium aenean pharetra magna ac. Scelerisque eleifend donec pretium vulputate. Diam phasellus vestibulum lorem sed risus. Leo duis ut diam quam nulla porttitor. Vitae suscipit tellus mauris a diam maecenas sed. Aliquam faucibus purus in massa tempor nec. Amet mattis vulputate enim nulla aliquet porttitor lacus luctus. Aenean pharetra magna ac placerat. Tellus in metus vulputate eu scelerisque felis imperdiet. Blandit cursus risus at ultrices mi.
  Massa placerat duis ultricies lacus sed turpis tincidunt id aliquet. Ut aliquam purus sit amet luctus venenatis lectus. Id venenatis a condimentum vitae sapien. At varius vel pharetra vel turpis nunc eget lorem. Eu tincidunt tortor aliquam nulla facilisi cras. Mi quis hendrerit dolor magna eget est lorem ipsum. Vel turpis nunc eget lorem dolor sed viverra ipsum. Non sodales neque sodales ut etiam sit amet nisl purus. Aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed. Sed sed risus pretium quam vulputate. Congue eu consequat ac felis donec. Risus ultricies tristique nulla aliquet enim tortor at auctor. Egestas egestas fringilla phasellus faucibus. Pretium fusce id velit ut. Interdum consectetur libero id faucibus nisl.
  Amet consectetur adipiscing elit pellentesque habitant. Amet consectetur adipiscing elit ut aliquam purus. Feugiat pretium nibh ipsum consequat nisl. Enim nulla aliquet porttitor lacus luctus. Ullamcorper dignissim cras tincidunt lobortis. Turpis egestas pretium aenean pharetra magna ac placerat vestibulum lectus. In vitae turpis massa sed elementum. Quis vel eros donec ac. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi. Non arcu risus quis varius quam quisque id diam vel. Odio ut enim blandit volutpat maecenas. Lobortis mattis aliquam faucibus purus in massa tempor. Urna cursus eget nunc scelerisque viverra.
  Rutrum tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras. Ornare arcu dui vivamus arcu. Odio morbi quis commodo odio aenean. Morbi tristique senectus et netus et malesuada fames ac turpis. Massa tincidunt dui ut ornare lectus sit. Sit amet nisl suscipit adipiscing bibendum est ultricies. Quis enim lobortis scelerisque fermentum dui faucibus in ornare quam. Pellentesque elit ullamcorper dignissim cras. Duis at consectetur lorem donec massa sapien faucibus et molestie. In metus vulputate eu scelerisque felis imperdiet proin. Quam elementum pulvinar etiam non quam lacus suspendisse faucibus interdum. Venenatis cras sed felis eget velit aliquet. Vitae justo eget magna fermentum. Imperdiet proin fermentum leo vel. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Amet venenatis urna cursus eget nunc scelerisque. Cursus vitae congue mauris rhoncus aenean. Lorem ipsum dolor sit amet consectetur adipiscing elit. A diam maecenas sed enim ut sem.
  Quam elementum pulvinar etiam non quam lacus suspendisse faucibus. Facilisis sed odio morbi quis commodo odio aenean sed. Hendrerit dolor magna eget est lorem. Id velit ut tortor pretium. Pellentesque pulvinar pellentesque habitant morbi tristique senectus et. Interdum posuere lorem ipsum dolor sit amet consectetur adipiscing. Tellus in hac habitasse platea dictumst. Nunc scelerisque viverra mauris in aliquam. Nunc sed velit dignissim sodales. Enim nec dui nunc mattis enim ut tellus. Felis donec et odio pellentesque diam volutpat. Laoreet suspendisse interdum consectetur libero id faucibus nisl tincidunt. Viverra accumsan in nisl nisi scelerisque eu ultrices.
  Orci porta non pulvinar neque laoreet suspendisse. Et tortor consequat id porta nibh venenatis cras. Turpis egestas pretium aenean pharetra magna. Consectetur libero id faucibus nisl tincidunt. Scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam. Senectus et netus et malesuada fames ac. Tincidunt augue interdum velit euismod in pellentesque. Laoreet sit amet cursus sit. Sed felis eget velit aliquet sagittis. Dui nunc mattis enim ut tellus elementum sagittis vitae et. Fermentum posuere urna nec tincidunt. Et netus et malesuada fames ac turpis egestas. Scelerisque in dictum non consectetur a erat. Lacinia at quis risus sed vulputate odio ut enim blandit. Vitae ultricies leo integer malesuada. Dictum varius duis at consectetur lorem donec massa sapien. Ultricies tristique nulla aliquet enim tortor. Congue mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper.
  Eget dolor morbi non arcu risus. Nisl rhoncus mattis rhoncus urna neque viverra justo. Arcu cursus euismod quis viverra nibh cras pulvinar mattis. Nibh cras pulvinar mattis nunc sed blandit libero volutpat sed. Id neque aliquam vestibulum morbi blandit cursus. Risus nullam eget felis eget nunc. Cursus sit amet dictum sit. Massa ultricies mi quis hendrerit dolor magna. Eget mauris pharetra et ultrices neque. Nibh sit amet commodo nulla facilisi nullam vehicula. Aliquam sem fringilla ut morbi tincidunt. Eu tincidunt tortor aliquam nulla facilisi cras fermentum odio eu. Ut pharetra sit amet aliquam id. Sit amet risus nullam eget. Tellus integer feugiat scelerisque varius morbi enim nunc faucibus a. Sed euismod nisi porta lorem mollis.

Nothing special. Here, we just export an object with two properties: title and content. The part we care about here is the content (we're just adding title to simulate a blog post being returned from a database). While this can be whatever you want, here, we've just generated a long string of Lorem Ipsum text for testing.

That's all we need here. Next, optionally, because our text is just plain strings, to give us a bit of formatting we're going to look at how to parse our content as a string of Markdown. This step is completely optional and purely for showing off our work in context. If you don't care about formatting the text, feel free to skip down to "Calculating read time".

Writing a parseMarkdown function

To make the display of our content a bit prettier, real quick we're going to wire up a function for taking in our Markdown string and parsing it to HTML.


import { Parser, HtmlRenderer } from 'commonmark';

export default (markdown = '') => {
  const parser = new Parser();
  const renderer = new HtmlRenderer();
  const parsedMarkdown = parser.parse(markdown);
  return renderer.render(parsedMarkdown);

Here, we're pulling in the commonmark dependency that we installed earlier (if you skipped this step, jump back up to to the top and make sure you install it), and specifically, two named exports: Parser and HtmlRenderer.

Inside of our function definition, we put these to use by first creating a new instance of Parser storing it as parser and a new instance of HtmlRenderer as renderer. These give us the raw tools for parsing the markdown string we've passed in as an argument to our function.

Next, we call the parser.parse() method from the parser instance we got back from new Parser(), passing in our markdown string. In return, we get back the parsedMarkdown as an AST (Abstract Syntax Tree) that can be handed to the renderer.render() method we got back from new HtmlRenderer(). From this, we expect to get back a string of HTML (in this case, a series of <p></p> tags).

Now, let's jump into getting our read time wired up.

Calculating read time

To organize our work and give us an easy demo, we're going to wire up a Joystick component as our test bed. Because this is fairly simple, we're going to spit the full code we'll need out below and then walk through it.


import ui from '@joystick.js/ui';
import parseMarkdown from '../../../lib/parseMarkdown';
import post from '../../../lib/post';

const Index = ui.component({
  methods: {
    calculateReadTime: (content = '') => {
      const averageWordsPerMinute = 250;
      const words = content?.trim()?.split(' ')?.length;
      const minutesToRead = Math.round(words / averageWordsPerMinute);

      return `${minutesToRead} Minutes`;
  css: `
    div {
      max-width: 800px;
      margin: 50px auto;

    time {
      background: yellow;
  render: ({ methods }) => {
    return `
        <time>Time to read: ${methods.calculateReadTime(post?.content)}</time>

export default Index;

The majority of what's here is purely for presentation. First, down in the render() method of our component, we're rendering some HTML to display the title of our post that we've imported up top along with its content, which we're passing to the parseMarkdown() function that we've imported up top as well (if you skipped this part, just render the plain content with ${post.content}).

The important part here is our call to methods.calculateReadTime(). To our render() method, we expect our component instance to be passed as an object. On that object, we anticipate a methods property which is set to the methods object defined up above on our component (here, instead of writing something like component.methods, we use JavaScript Destructuring to "pluck off" methods as a single variable).

To methods.calculateReadTime(), all we need to do is pass the plain, unparsed value for post.content (if we passed the HTML we get back from parseMarkdown(), our word count would be off a little bit).

Focusing on the calculateReadTime() method we're defining under the methods object up top, we begin by taking in our content as a string. Next, inside of the function body, we set a constant for averageWordsPerMinute which is a rough estimate of the average time for a human to read text (238 WPM for non-fiction and 260 WPM for fiction with an average of 249 which we round up to 250).

Next, to get the number of words in our content, we take our content and call .trim() to strip off any begining and ending whitespace, and then call .split() on it to say "at every space in the string, create a new array item." The end result being—roughly—that we get 1 array item per word in our content. To get the total words, then, we just get the .length property of the array returned by .split(). Easy!

Finally, to calculate the minutesToRead, we just divide words by averageWordsPerMinute, using Math.round() to avoid any nasty floats like 5.0000092348. Once we have this, from our function, we return a string that concatenates the minutesToRead with the text "Minutes" for display back in our component.

Done! Now we know how long our solution to dogs chasing squirrels will take to read.

Wrapping up

In this tutorial, we learned how to write a simple function for calculating read time with JavaScript. To contextualize our work, we created a dummy blog post. We learned how to wire up a function for parsing the Markdown content in our post, and then, how to wire up a Joystick component where we could test out rendering our content and calculating the read time for that content.