tutorial // Apr 22, 2022

Creating a Loader Button in Joystick

How to create a button component with a dynamic loading state based on props.

Creating a Loader Button in Joystick

Getting started

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):

Terminal

npm i -g @joystick.js/cli

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

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

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

Adding icons

Before we dig into our loader button, we want to add support for the Font Awesome icon library. From this, we'll use a "spinner" icon that we can display when our button is in a loading state.

If you don't have a Font Awesome account, head over to the site and set up an account (they're not the spammy type so no worries on getting bombarded). Once you're logged in, you should be redirected to a screen showing a "Kit Code" which is a script tag that we need to add to our app.

VrctF8VwRD1H7uXN/Wfs73n7mJWYYrkE0.0
Copying a kit code on Font Awesome.

If you do already have a Font Awesome account, just head over to the Kits page and you will see a blue "New Kit +" button in the top-right corner of the page. Click this to generate a script tag similar to what you see above.

Once you have access to your Kit Code, copy it, and open up the Joystick project we just created in your IDE. From there, we want to open up the /index.html file at the root of the project.

/index.html

<!doctype html>
<html class="no-js" lang="en">
  <head>
    <meta charset="utf-8">
    <title>Joystick</title>
    <meta name="description" content="An awesome JavaScript app that's under development.">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="theme-color" content="#FFCC00">
    <link rel="apple-touch-icon" href="/apple-touch-icon-152x152.png">
    <link rel="stylesheet" href="/_joystick/index.css">
    <link rel="manifest" href="/manifest.json">
    <script src="https://kit.fontawesome.com/8c0c20c9e4.js" crossorigin="anonymous"></script>
    ${css}
  </head>
  <body>
    <div id="app"></div>
    ${scripts}
    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("/service-worker.js");
      }
    </script>
  </body>
</html>

Inside that file, just above the ${css} tag, we want to paste in the <script></script> tag we just copied from Font Awesome. Once this is done, save the file and now Font Awesome will be loaded globally throughout the app.

Implementing a loader button

The remainder of our work for this tutorial is going to be focused on building out a reusable button component with an internal loading state. Our goals will be:

  1. Have a button with two states: loading and not loading.
  2. A way to call a function that will do some work relevant to our button.
  3. A callback that we can call to tell the button that our work is done.

To start, let's create a fresh skeleton component in /ui/components/loaderButton/index.js:

/ui/components/loaderButton/index.js

import ui from '@joystick.js/ui';

const LoaderButton = ui.component({
  render: () => {
    return `
      <button>
      </button>
    `;
  },
});

export default LoaderButton;

Here, we're creating a Joystick component using the @joystick.js/ui library with a single option render which returns a <button></button> tag for its markup.

/ui/components/loaderButton/index.js

import ui from '@joystick.js/ui';

const LoaderButton = ui.component({
  defaultProps: {
    label: 'Button',
    loadingLabel: 'Loading...',
  },
  state: {
    loading: false,
  },
  render: () => {
    return `
      <button>
      </button>
    `;
  },
});

export default LoaderButton;

Next, we want to add two minor details: a defaultProps option and state option. For defaultProps, we're anticipating a label prop being passed to our component. Here, we say "if no label or loadingLabel prop are passed, replace them with their default provided here." Similarly, for state, we're setting the default value of loading on state within the component. As we'll see next, this will come into play when we update our markup below to change what's rendered based on our state and props.

/ui/components/loaderButton/index.js

import ui from '@joystick.js/ui';

const LoaderButton = ui.component({
  defaultProps: {
    label: 'Button',
    loadingLabel: 'Loading...',
  },
  state: {
    loading: false,
  },
  render: ({ state, when, props }) => {
    return `
      <button ${state.loading ? 'disabled' : ''} class="button ${state.loading ? 'is-loading' : ''}">
        ${when(state.loading, `<i class="fas fa-circle-notch fa-spin"></i>`)} ${state.loading ? props.loadingLabel : props.label}
      </button>
    `;
  },
});

export default LoaderButton;

Now for the important part. Here, we've expanded the markup returned by our render() function to include the conditional logic necessary to mutate the state of our button relative to props and state.

Because the render() function is returning a string of HTML, here, we take advantage of JavaScript string interpolation (a way to evaluate a variable and return its result inside of a string) to dynamically build the HTML that will represent the current state of our button.

From the opening <button tag, the first statement we see is ${state.loading ? 'disabled' : ''}. This is saying "if the current value of state.loading is true, return a string with disabled inside of it, and otherwise, return an empty string." To get access to state, we pull it from the component instance passed to our render() function. Here, we use JavaScript destructuring to "pluck apart" that value, exposing the properties defined on it as variables directly inside of our render function.

In terms of what we're doing here, if our button is in a loading state, we want to disable it to prevent additional clicks while the work we've assigned to that button is completed. Here, we dynamically add the disabled attribute to our <button></button> tag based on the value of state.loading. So, if we're loading, disable the button, and if we're not, make it active/clickable.

To the right of this, using the same concept with ${state.loading ? 'is-loading' : ''}, saying "if state.loading is true, we want to dynamically add a CSS class to our <button></button> called is-loading." This will allow us to add some CSS styling later based on the loading state of the button.

On the next line (now inside of our <button></button> tag), we use a special function (known as a "render function" in Joystick) called when() to conditionally render the loading icon (we've chosen the Circle Notch icon from Font Awesome which includes a built-in animation class fa-spin) for our button if the value of state.loading is true. The first argument passed to when() is the value we want to "test" for truthiness and the second value is a string of HTML to render if the first value is true.

Finally, we use the same interpolation syntax as the first to conditionally render the label for our <button></button>, just to the right of our icon. Here, we say if state.loading is true, we want to render the loadingLabel value from props, otherwise, we want to just render the regular label prop.

/ui/components/loaderButton/index.js

import ui from '@joystick.js/ui';

const LoaderButton = ui.component({
  defaultProps: {
    label: 'Button',
    loadingLabel: 'Loading...',
  },
  state: {
    loading: false,
  },
  css: `
    .button {
      padding: 20px;
      border: none;
      background: #333;
      color: #fff;
      border-radius: 3px;
      font-size: 15px;
      cursor: pointer;
    }

    .button:active {
      position: relative;
      top: 1px;
    }

    .button i {
      margin-right: 5px;
    }

    .button.is-loading,
    .button:disabled {
      opacity: 0.9;
      pointer-events: none;
    }
  `,
  render: ({ state, when, props }) => {
    return `
      <button ${state.loading ? 'disabled' : ''} class="button ${state.loading ? 'is-loading' : ''}">
        ${when(state.loading, `<i class="fas fa-circle-notch fa-spin"></i>`)} ${state.loading ? props.loadingLabel : props.label}
      </button>
    `;
  },
});

export default LoaderButton;

Getting into the final details. Here, we've added the necessary CSS styles for our button. Here, we've defined styles for a simple black button that "bounces" down when clicked (to simulate the depth of a physical button) and has its opacity change to 90% with no hover/click interactions when it's in a loading or disabled state.

/ui/components/loaderButton/index.js

import ui from '@joystick.js/ui';

const LoaderButton = ui.component({
  defaultProps: {
    label: 'Button',
    loadingLabel: 'Loading...',
  },
  state: {
    loading: false,
  },
  css: `...`,
  events: {
    'click button': (event, component) => {
      if (component.props.onClick) {
        component.setState({ loading: true }, () => {
          component.props.onClick(event, () => {
            component.setState({ loading: false });
          });
        });
      }
    }
  },
  render: ({ state, when, props }) => {
    return `
      <button ${state.loading ? 'disabled' : ''} class="button ${state.loading ? 'is-loading' : ''}">
        ${when(state.loading, `<i class="fas fa-circle-notch fa-spin"></i>`)} ${state.loading ? props.loadingLabel : props.label}
      </button>
    `;
  },
});

export default LoaderButton;

Now for the important part. Our last bit of work for our component: handling click events. Here, we've added the events option to our component which helps us to define JavaScript event listeners. On the object passed to events, we define an event listener by first specifying a property name in the form of an <event> <selector> pattern where <event> is the type of DOM event we want to listen for and <selector> is the element we want to listen for the event on.

To that property, we assign the function that's called when the specified event is detected on the specified selector. To that function, we receive two arguments: the raw DOM event that took place and our component instance.

Inside of the function here, we first check to see if we've been passed an onClick function via props. This is important. This is the function we want to call to do the work that will determine the loading state of our button (e.g., uploading a file, saving a change, etc). If that function exists, first, we make sure to set state.loading to true using the .setState() method on our component instance (we pass that function an object with the state properties we want to update, along with their new values).

As the second argument to this, we pass a callback to fire after state.loading is set to true. Inside of that we call to the onClick function passed via props, handing it the DOM event that fired and as a second argument, a function to call once any work is "done."

Inside of that function, notice that we revert state.loading back to false. The idea being that once the work is signaled as being "done," we don't want to show our button in a loading state any longer (i.e., we want to make it clickable).

Now for the fun part, let's take our component and put it to use.

Using the loader button

To test out our loader button, we're going to modify the component located at /ui/pages/index/index.js as it's already wired up to the root route of our app.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import LoaderButton from '../../components/loaderButton';

const Index = ui.component({
  render: ({ component }) => {
    return `
      <div>
        ${component(LoaderButton, {
          label: 'Start Machine',
          loadingLabel: 'Starting machine...',
          onClick: (_event, callback) => {
            setTimeout(() => {
              if (callback) callback();
            }, 3000);
          },
        })}
      </div>
    `;
  },
});

export default Index;

Here, we're overwriting the existing contents of this file completely. Up top, we've imported our LoaderButton component. Down in the render() function for our Index component, we've "plucked off" the component() render function to help us render our LoaderButton component on the Index page.

To that function, we pass the LoaderButton component as we imported it at the top of the file. And as a second argument, we pass an object of props that we want to pass down to our component. If we look here, we can see the three props we expect: label, loadingLabel, and onClick.

For the onClick, we taken in the DOM event we expect (here, we prefix the argument name with an _ underscore to suggest that we're not going to use the variable in our code. After this, we take in our "done" callback. Here, to simulate doing some work, we've added a setTimeout() for 3 seconds (the 3000 is 3 seconds in milliseconds) and inside, if we were passed a callback, we call it.

Simple as that! Now, if we load up http://localhost:2600 in our browser, we should see our button, and if we click it, we should see it shift to its loading state.

Wrapping up

In this tutorial, we learned how to create a loading button with Joystick. We learned how to add icons using a CDN link from Font Awesome, and then, how to wire up a custom component that could receive props when used to change the label, loading text, and onClick functionality when the button was clicked. Finally, we learned how to put the button to use, using a setTimeout() to demonstrate some long-running work and shifting our button back to its default state after loading was complete.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode