tutorial // Apr 07, 2023

How to Create a Stripe Elements Card Component in Joystick

How to create a Joystick component that renders a Stripe Elements credit card input.

How to Create a Stripe Elements Card Component in Joystick

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.

Accessing our Stripe API keys

Before we dig into the code, for this tutorial, we're going to need access to a Stripe account. Head over to the signup page on their site and create an account if you haven't already.

Once you have an account, login to the dashboard. It should look something like this:

https://cheatcode-post-assets.s3.us-east-2.amazonaws.com/OWpbZnbl26eayuQc/Screen%20Shot%202023-04-07%20at%209.20.56%20AM.png
Viewing your API keys in the Stripe dashboard.

Where we want to navigate to is the page pictured above. To get there:

  1. In the top-right corner, make sure that you've toggled the "Test mode" toggle so that it's lit up (as of writing this will turn orange when activated).
  2. To the left of that toggle, click the "Developers" button.
  3. On the next page, in the top navigation menu, select the "API keys" tab.
  4. Under the "Standard keys" block on this page, locate your "Publishable key."
  5. Copy this key (don't worry, it's intended to be exposed to the public).

Next, once we have our publishable key, we need to open up the project we just created and navigate to the settings.development.json file at the root of the project:

settings.development.json

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      }
    ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": {
      "from": "",
      "smtp": {
        "host": "",
        "port": 587,
        "username": "",
        "password": ""
      }
    }
  },
  "global": {},
  "public": {
    "stripe": {
      "publishableKey": "pk_test_abc123"
    }
  },
  "private": {}
}

In this file, we want to add a property stripe to the public object (these are settings that Joystick—the framework we're using for this tutorial—automatically makes accessible in the browser), and on that, set a property publishableKey, setting its value to a string containing the "Publishable key" we just copied from the Stripe dashboard.

Once that's set, we're ready to get our credit card component set up.

Creating a credit card component

In order to create our credit card component, we're going to be using the Stripe Elements library. This is a UI component library offered by Stripe that gives us access to a collection of different components for collecting credit cards and other payment information from users.

To access the library, Stripe offers a CDN (content delivery network) link where we can load their Stripe JS library from (this library includes Stripe Elements along with some other helpers for working with Stripe).

This is where we'll start. First, we're going to create an empty component and then work through connecting Stripe JS:

/ui/components/creditCard/index.js

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

const CreditCard = ui.component({
  css: `
    .card-input {
      border: 1px solid #eee;
      padding: 20px;
      border-radius: 3px;
      margin-bottom: 20px;
    }
  `,
  render: ({ props }) => {
    return `
      <div class="card-input" id="${props.id || 'card-element'}"></div>
    `;
  },
});

export default CreditCard;

Getting us started, here, we're creating a base Joystick component using the ui.component() function imported from @joystick.js/ui package (automatically installed for us when we ran joystick create app earlier). To that function, we pass an object that defines our component's behavior.

Here, we've added a render function which returns a string of HTML for our component and a css string which defines the CSS styles for our component.

Focusing on the render function, first, we use JavaScript destructuring to "pluck off" the props field from the component instance (in other words, the object representing the current component in memory) passed as the first (and only) argument to the render function. So that's clear, we could also write the above like this:

  render: (componentInstance) => {
    return `
      <div class="card-input" id="${componentInstance.props.id || 'card-element'}"></div>
    `;
  },

For the HTML returned from the render function, we're providing a "placeholder" element that Stripe Elements will target and replace with our credit card form. Here, we use a <div></div> tag with a class card-input (we use this to apply the CSS styling we defined in the css option on our component above) and we allow for a dynamic id attribute to be passed into our component via the props argument (if one isn't passed, we default to card-element for the id).

/ui/components/creditCard/index.js

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

const CreditCard = ui.component({
  lifecycle: {
    onMount: (component) => {
      component.methods.handleLoadStripe(() => {
        // Stripe.js is loaded and we can use Stripe Elements...
      });
    },
  },
  css: `...`,
  render: ({ props }) => {
    return `
      <div class="card-input" id="${props.id || 'card-element'}"></div>
    `;
  },
});

export default CreditCard;

Like we hinted at above, in order to get access to Stripe Elements, we need to load the Stripe JS library via their CDN first. Because we're building a reusable component, we need to keep in mind that it could be loaded multiple times on a single page. By default, Stripe JS spits out a warning if we do this, so now, we want to write a function that only loads Stripe JS if it hasn't already been loaded on the page.

To do that, here, we're going to write a function that's called whenever our CreditCard component is mounted. Above, we've added the lifecycle option to our component definition, set to an object with an onMount function which Joystick will automatically call as soon as our component is rendered in the browser. Inside, we're calling to component.methods.handleLoadStripe(), a function that we'll define next that takes a callback to fire after Stripe has been loaded.

/ui/components/creditCard/index.js

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

const CreditCard = ui.component({
  lifecycle: {
    onMount: (component) => {
      component.methods.handleLoadStripe(() => {
        // Stripe.js is loaded and we can use Stripe Elements...
      });
    },
  },
  methods: {
    handleLoadStripe: (callback = null) => {
      let stripeJSScript = document.body.querySelector('#stripejs');

      if (!stripeJSScript) {
        stripeJSScript = document.createElement("script");
        stripeJSScript.src = "https://js.stripe.com/v3/";
        stripeJSScript.id = "stripejs";
        document.body.appendChild(stripeJSScript);
      }

      if (!window.stripe) {
        stripeJSScript.onload = () => {
          window.stripe = Stripe(joystick.settings.public.stripe.publishableKey);
          if (callback) callback(window.stripe);
        };
      }

      if (window.stripe && callback) {
        callback(window.stripe);
      }
    },
  },
  css: `...`,
  render: ({ props }) => {
    return `
      <div class="card-input" id="${props.id || 'card-element'}"></div>
    `;
  },
});

export default CreditCard;

Above, we've added another property to our component, methods which is set to an object of miscellaneous functions for performing work in our component. On that object, we've defined the handleLoadStripe() function that we call to up in our lifecycle.onMount function. Here, we've dumped out the entirety of the function to make it easier to understand everything in context.

Starting at the top, we take in the callback function we alluded to earlier as the only argument to our function (as we'll see, we'll call this once Stripe JS has been loaded).

Next, just inside of our function body, we attempt to locate an existing instance of Stripe JS that has already been loaded in the browser. To do it, we use the id selector #stripejs. If we find a <script></script> tag with this id in the body, that means we've already loaded Stripe JS and do not need to do it again.

If we do not have Stripe defined, we want to dynamically create a <script></script> tag in memory and then append it to the <body></body> of the site (upon doing this, the browser will automatically attempt to load Stripe JS). Here, we do that by calling to document.createElement('script') to create that tag and then dynamically set the src and id on that element in-memory. Finally, we append that script into the DOM (document object model) using document.body.appendChild() and passing in the stripeJSScript we just created.

Below this, we want to anticipate the loading of Stripe so that we can fire the callback we passed in to handleLoadStripe(). Assuming that we haven't loaded Stripe JS before (meaning window.stripe, where we'll set our instance of Stripe JS, is undefined), we want to attach the onload function to the stripeJSScript tag that we just created in memory. Once the browser has signaled that this script has been loaded, this function will be called.

Inside, we want to set window.stripe to a call to Stripe() (the global value that we get by loading Stripe JS via the script tag we created above). To that call, we pass the publishableKey we added to our settings.development.json file earlier, accessing it via the global—meaning we don't need to import it—joystick.settings object. Just after this, if a callback was passed, we call it passing in window.stripe for the sake of convenience.

Finally, at the bottom of our function, assuming that we've already done this work before and window.stripe is defined, if we have a callback, we go ahead and call it, again passing in window.stripe.

/ui/components/creditCard/index.js

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

const CreditCard = ui.component({
  lifecycle: {
    onMount: (component) => {
      component.methods.handleLoadStripe(() => {
        const cardSelector = component.props.id ? `#${component.props.id}` : '#card-element';
        const target = document.body.querySelector(cardSelector);
        const previouslyMounted = !!(window?.joystick?._external?.stripe && window?.joystick?._external?.stripe[cardSelector]?.mounted);

        if (target && !previouslyMounted) {
          const elements = window.stripe.elements();
          const cardElement = elements.create("card");
          
          cardElement.mount(cardSelector);
          
          window.joystick._external.stripe = {
            ...(window.joystick?._external?.stripe || {}),
            [cardSelector]: {
              element: cardElement,
              mounted: true,
            }
          };
        }
      });
    },
  },
  methods: {
    handleLoadStripe: (callback = null) => { ... },
  },
  css: `...`,
  render: ({ props }) => {
    return `
      <div class="card-input" id="${props.id || 'card-element'}"></div>
    `;
  },
});

export default CreditCard;

Now for the important part. Back up in our lifecycle.onMount function, in the callback we passed to handleLoadStripe(), now we want to wire up mounting the Stripe Elements card input in place of our <div class="card-input"></div> from our render() function.

There's some nuance, though. Because Joystick components are dynamically rendered, we need to keep in mind whether or not we've already mounted Stripe Elements into the DOM. The reason why is that if the parent component where our CreditCard component is rendered itself re-renders, it will re-render all of its children, too (including CreditCard).

Because we're calling this code onMount, whenever that child re-renders, its onMount function will be called. Because we do not control the input Stripe is injecting into the page, we need to check whether or not we've already mounted it. If we don't, we risk creating a jarring experience for our users where they type in a card number and then on re-render of the parent, the input gets wiped out because Stripe Elements is re-mounted.

To get around this, here, we begin by creating a variable cardSelector which checks to see if we've been passed an id via the component.props value (here, the component argument passed to onMount is identical to the one we destructured earlier down in the render function) and conditionally creates a selector string that we can pass to document.body.querySelector() on the next line.

With this, we then use a little bit of special sauce from Joystick. Unfortunately, Stripe doesn't give us a way to check if we've already mounted the card input in the browser. To get around this, we can leverage the window.joystick._external object which is a convenience object we get from Joystick for tracking the state of third-party dependencies.

Here, we know that we've previously mounted this input if window.joystick._external.stripe is defined and on that object, we've defined an object set to the cardSelector with the property mounted set to true on it.

Utilizing this, next, if we have a target and previouslyMounted is false, we want to mount our card input. To do it, we create an instance of Stripe Elements using window.stripe.elements(), storing the result in the variable elements. From this, we can create our cardElement in memory with elements.create("card"). Next, to actually render the card input on screen, we call to cardElement.mount() passing in the cardSelector we derived at the top of our function.

Last but not least, because we need to keep track of whether or not we've previously mounted the input, we make sure to update the window.joystick._external.stripe object we referenced above. Here, we overwrite the current value of that with a copy of itself, and on the end, add a new object for the cardSelector, setting element to our cardElement instance and mounted to true so subsequent re-renders do not re-mount the card input.

That does it for our component, let's put this to use!

Utilizing our Credit Card component

For this next part, we're going to do a code dump and walk through everything together. In the /ui/pages/index/index.js file, we want to replace the existing contents with the following:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import CreditCard from '../../components/creditCard';

const Index = ui.component({
  css: `
    .credit-card-error {
      background: red;
      color: #fff;
      padding: 15px;
      line-height: 21px;
      border-radius: 3px;
    }
  `,
  events: {
    'click .process-payment': (_event, component = {}) => {
      stripe.createToken(window.joystick._external?.stripe['#credit-card']?.element)
        .then((result) => {
          if (result?.error) {
            return component.setState({ creditCardError: result?.error?.message });
          }

          component.setState({ creditCardError: null }, () => {
            console.log(result);
          });
        });
    },
  },
  render: ({ component, state, when }) => {
    return `
      <div>
        ${component(CreditCard, {
          id: 'credit-card',
        })}
        ${when(state.creditCardError, `
          <p class="credit-card-error">${state.creditCardError}</p>
        `)}
        <button class="process-payment">Buy Now</button>
      </div>
    `;
  },
});

export default Index;

Just like we saw earlier, here, we're defining a Joystick component, however this time, we're rendering that component as a page (the difference being how we render the component, not how we write the component). Though we don't see it, inside of the /index.server.js file at the root of our project, a route is defined which renders this component for the root or index / route of our app. Whenever that route is visited, we get the above component rendered as the page.

For this page, we want to render our CreditCard component. To do it, we use the component render method included on the component instance passed to the render function on our component. To it, we pass the imported CreditCard component definition from /ui/components/creditCard/index.js.

Beneath this, we anticipate a potential error being returned by Stripe when we attempt to process a bad credit card, rendering it conditionally using the when render method which will only render the string of HTML passed as the second argument if the first returns true.

Finally, we render a button with a class process-payment that we can attach a click event to where we hand off the card input to Stripe to get back a card token (what Stripe uses behind the scenes to process payments).

To stitch this all together, up in the events object, we've defined a click event listener on the .process-payment class we added to our <button></button>. Inside, we make a call to the global stripe value that we set (anything set on window like window.stripe can be accessed without window. if you wish) and its .createToken() method. To it, we pass the window.joystick._external?.stripe['#credit-card']?.element value that we set inside of our CreditCard component, using the credit-card id that we anticipate being set due to the id prop we passed to it when rendering it down below.

From there, Stripe takes over. If our card is invalid, we expect to get back result.error which we set the message of on to the state object of our component as creditCardError. If there is no error, we make sure to first clear it out on state and then log out the result we get back from Stripe.

That's it! If we play around with the input passing incomplete values, we'll see the error message rendered beneath our card input but, the card input will not be remounted (keeping the user's input intact).

Wrapping up

In this tutorial, we learned how to build a credit card component using Stripe Elements inside of a Joystick component to render a credit card input. We learned how to dynamically load Stripe JS to give us access to Stripe Elements, and then, how to safely mount the Stripe Elements card input conditionally based on whether or not we did it before. Finally, we learned how to access that card input via the global joystick._external object, passing it off to stripe.createToken() to generate a card token for payment processing.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode