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.
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:
Where we want to navigate to is the page pictured above. To get there:
- 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).
- To the left of that toggle, click the "Developers" button.
- On the next page, in the top navigation menu, select the "API keys" tab.
- Under the "Standard keys" block on this page, locate your "Publishable key."
- 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.