DoneJS StealJS jQuery++ FuncUnit DocumentJS
5.33.3
6.0.0 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Credit Card

  • Edit on GitHub

This beginner guide walks through building a very simple credit card payment form. It uses Stripe.js v2 API to create a token which can be used to create a charge. It also performs simple validation on the payment form values.

In this guide, you will learn how to:

  • Set up a basic CanJS application.
  • Collect form data and post it to a service endpoint when the form is submitted.
  • Do basic validation.

The final widget looks like:

See the Pen Credit Card Guide (Simple) [Finished] by Bitovi (@bitovi) on CodePen.

To use the widget:

  1. Enter a Card Number, Expiration Date, and CVC.
  2. Click on the form so those inputs lose focus. The Pay button should become enabled.
  3. Click the Pay button to get a token from Stripe, which could be used to create a credit card payment.
  4. Change the inputs to invalid values. An error message should appear, the invalid inputs should be highlighted red, and the Pay button should become disabled.

START THIS TUTORIAL BY CLONING THE FOLLOWING CODEPEN:

See the Pen Credit Card Guide (Simple) [Starter] by Bitovi (@bitovi) on CodePen.

This CodePen has initial prototype HTML and CSS which is useful for getting the application to look right.

The following sections are broken down into:

  • The problem — A description of what the section is trying to accomplish.
  • What you need to know — Information about CanJS that is useful for solving the problem.
  • The solution — The solution to the problem.

Setup

The problem

Let’s create a cc-payment component with a ViewModel, which will have an amount property that defaults to 9.99. When complete, we should be able update the displayed “pay amount”.

What you need to know

  • To use Stripe, you must call Stripe.setPublishableKey.

  • A basic CanJS setup uses instances of a can-component, which glues a ViewModel to a View in order to manage it's behavior as follows:

    import { Component } from "can";
    // Define the Component
    const CCPayment = Component.extend({
      tag: "cc-payment",
      view: "...",
      ViewModel: {}
    });
    
  • CanJS component will be mounted in the DOM by adding the the component tag in the HTML page:

    <cc-payment></cc-payment>
    
  • CanJS component uses can-stache to render data in a template and keep it live.

  • The ViewModel is an instance of can-define/map/map allows you to define a property with a default value like:

    const ProductVM = DefineMap.extend("ProductVM", {
      age: {default: 34}
    })
    

    This lets you create instances of that type, get & set those properties, and listen to changes like:

    const productVM = new ProductVM({});
    
    productVM.age //-> 34
    
    productVM.on("age", function(ev, newAge){
      console.log("person age changed to ", newAge);
    });
    
    productVM.age = 35 //-> logs "person age changed to 35"
    

The solution

Update the HTML tab to:

<cc-payment></cc-payment>
<script src="https://js.stripe.com/v2/"></script>

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Stripe.setPublishableKey("pk_test_zCC2JrO3KSMeh7BB5x9OUe2U");

Component.extend({
    tag: 'cc-payment',
    view: `
        <form>

            <input type="text" name="number" placeholder="Card Number"/>

            <input type="text" name="expiry" placeholder="MM-YY"/>

            <input type="text" name="cvc" placeholder="CVC"/>

            <button>Pay $\{{ amount }}</button>

        </form>
    `,
    ViewModel: {
        amount: { default: 9.99 }
    }
});

Read form values

The problem

Let’s send the form values to the ViewModel so we can process and validate them. In this step, we’ll send the form values to the ViewModel and print out the values to make sure the ViewModel has them correctly.

Print out the exported values like:

<p>{{userCardNumber}}, {{userExpiry}}, {{userCVC}}</p>

What you need to know

  • Use value:bind to set up a two-way binding in can-stache. For example, the following keeps email on the ViewModel and the input’s value in sync:

    <input value:bind="email"/>
    
  • DefineMap.extend allows you to define a property by defining its type like so:

    import { DefineMap } from "can";
    
    const Person = DefineMap.extend("Person", {
      name: "string",
      age: "number"
    })
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Stripe.setPublishableKey("pk_test_zCC2JrO3KSMeh7BB5x9OUe2U");

Component.extend({
    tag: 'cc-payment',
    view: `
        <form>

            <input type="text" name="number" placeholder="Card Number"
                value:bind="this.userCardNumber"/>

            <input type="text" name="expiry" placeholder="MM-YY"
                value:bind="this.userExpiry"/>

            <input type="text" name="cvc" placeholder="CVC"
                value:bind="this.userCVC"/>

            <button>Pay $\{{ this.amount }}</button>

            <p>{{ this.userCardNumber }}, {{ this.userExpiry }}, {{ this.userCVC }}</p>
            
        </form>
    `,
    ViewModel: {
        amount: {
            default: 9.99
        },

        userCardNumber: "string",

        userExpiry: "string",

        userCVC: "string"
    }
});

Format form values

The problem

Our data needs to be cleaned up before we pass it to the server. We need to create the following properties, with associated behaviors:

  • cardNumber - The user’s card number as a string without hyphens (-).
  • expiryMonth - A number for the month entered.
  • expiryYear - A number for the year entered.
  • cvc - A number for the cvc entered.

So that we can print out the values like:

<p>{{cardNumber}}, {{expiryMonth}}-{{expiryYear}}, {{cvc}}</p>

What you need to know

  • ES5 Getter Syntax can be used to define a DefineMap property that changes when another property changes. For example, the following defines a firstName property that always has the first word of the fullName property:

    const Person = DefineMap.extend({
      fullName: "string",
      get firstName(){
        return this.fullName.split(" ")[0];
      }
    });
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Stripe.setPublishableKey("pk_test_zCC2JrO3KSMeh7BB5x9OUe2U");

Component.extend({
    tag: 'cc-payment',
    view: `
        <form>

            <input type="text" name="number" placeholder="Card Number"
                value:bind="this.userCardNumber"/>

            <input type="text" name="expiry" placeholder="MM-YY"
                value:bind="this.userExpiry"/>

            <input type="text" name="cvc" placeholder="CVC"
                value:bind="this.userCVC"/>

            <button>Pay $\{{ this.amount }}</button>

            <p>{{ this.userCardNumber }}, {{ this.userExpiry }}, {{ this.userCVC }}</p>
            <p>{{ this.cardNumber }}, {{ this.expiryMonth }}-{{ this.expiryYear }}, {{ this.cvc }}</p>
            
        </form>
    `,
    ViewModel: {
        amount: {
            default: 9.99
        },

        userCardNumber: "string",
        get cardNumber() {
            return this.userCardNumber ? this.userCardNumber.replace(/-/g, ""): null;
        },

        userExpiry: "string",
        get expiryParts() {
            if(this.userExpiry) {
                return this.userExpiry.split("-").map(function(p){
                    return parseInt(p, 10);
                });
            }
        },
        get expiryMonth() {
            return this.expiryParts && this.expiryParts[0];
        },
        get expiryYear() {
            return this.expiryParts && this.expiryParts[1];
        },

        userCVC: "string",
        get cvc() {
            return this.userCVC ?
            parseInt(this.userCVC, 10) : null;
        }
    }
});

Validate individual form values

The problem

We need to add class="is-error" when a form value has a value that is not valid according to Stripe’s validators. To do that, we need to create the following properties that will return an error message for their respective form property:

  • cardError - “Invalid card number (ex: 4242-4242-4242).”
  • expiryError - “Invalid expiration date (ex: 01-22).”
  • cvcError - “Invalid CVC (ex: 123).”

What you need to know

  • Stripe has validation methods:

    • Stripe.card.validateCardNumber(number)
    • Stripe.card.validateExpiry(month, year)
    • Stripe.card.validateCVC(cvc)
  • Use {{#if(value)}} to do if/else branching in can-stache.

    {{#if(error)}}class="is-error"{{/if}}
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Stripe.setPublishableKey("pk_test_zCC2JrO3KSMeh7BB5x9OUe2U");

Component.extend({
    tag: 'cc-payment',
    view: `
        <form>

            <input type="text" name="number" placeholder="Card Number"
                {{# if(this.cardError) }}class="is-error"{{/ if }}
                value:bind="this.userCardNumber"/>

            <input type="text" name="expiry" placeholder="MM-YY"
                {{# if(this.expiryError) }}class="is-error"{{/ if }}
                value:bind="this.userExpiry"/>

            <input type="text" name="cvc" placeholder="CVC"
                {{# if(this.cvcError) }}class="is-error"{{/ if }}
                value:bind="this.userCVC"/>

            <button>Pay $\{{ this.amount }}</button>

            <p>{{ this.userCardNumber }}, {{ this.userExpiry }}, {{ this.userCVC }}</p>
            <p>{{ this.cardNumber }}, {{ this.expiryMonth }}-{{ this.expiryYear }}, {{ this.cvc }}</p>

        </form>
    `,
    ViewModel: {
        amount: { default: 9.99 },

        userCardNumber: "string",
        get cardNumber() {
            return this.userCardNumber ? this.userCardNumber.replace(/-/g, ""): null;
        },
        get cardError() {
            if( this.cardNumber && !Stripe.card.validateCardNumber(this.cardNumber) ) {
                return "Invalid card number (ex: 4242-4242-4242).";
            }
        },

        userExpiry: "string",
        get expiryParts() {
            if(this.userExpiry) {
                return this.userExpiry.split("-").map(function(p){
                    return parseInt(p, 10);
                });
            }
        },
        get expiryMonth() {
            return this.expiryParts && this.expiryParts[0];
        },
        get expiryYear() {
            return this.expiryParts && this.expiryParts[1];
        },
        get expiryError() {
            if( (this.expiryMonth || this.expiryYear) &&
                 !Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) ) {
                return "Invalid expiration date (ex: 01-22).";
            }
        },

        userCVC: "string",
        get cvc() {
            return this.userCVC ?
                parseInt(this.userCVC, 10) : null;
        },
        get cvcError() {
            if( this.cvc && !Stripe.card.validateCVC(this.cvc) ) {
                return "Invalid CVC (ex: 123).";
            }
        }
    }
});

Get payment token from Stripe

The problem

When the user submits the form, we need to call Stripe to get a token that we may use to charge the credit card. When we get a token, we will simply alert it to the user like:

alert("Token: " + response.id);

After submitting the form, you should see an alert like:

Alert

What you need to know

  • Use on:event to listen to an event on an element and call a method in can-stache. For example, the following calls doSomething() when the <div> is clicked:

    <div on:click="doSomething(scope.event)"> ... </div>
    

    Notice that it also passed the event object with scope.event.

  • To prevent a form from submitting, call event.preventDefault().

  • Stripe.card.createToken can be used to get a token that can be used to charge a card:

    Stripe.card.createToken({
      number: this.cardNumber,
      cvc: this.cvc,
      exp_month: this.expiryMonth,
      exp_year: this.expiryYear
    }, stripeResponseHandler(status, response) )
    
    • stripeResponseHandler gets called back with either:
      • success: a status of 200 and a response with an id that is the token.
      • failure: a status other than 200 and a response with an error.message value detailing what went wrong.

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Stripe.setPublishableKey("pk_test_zCC2JrO3KSMeh7BB5x9OUe2U");

Component.extend({
    tag: 'cc-payment',
    view: `
        <form on:submit="this.pay(scope.event)">

            <input type="text" name="number" placeholder="Card Number"
                    {{# if(this.cardError) }}class="is-error"{{/ if }}
                value:bind="this.userCardNumber"/>

            <input type="text" name="expiry" placeholder="MM-YY"
                {{# if(this.expiryError) }}class="is-error"{{/ if }}
                value:bind="this.userExpiry"/>

            <input type="text" name="cvc" placeholder="CVC"
            {{# if(this.cvcError) }}class="is-error"{{/ if }}
                value:bind="this.userCVC"/>

            <button>Pay $\{{ this.amount }}</button>

            <p>{{ this.userCardNumber }}, {{ this.userExpiry }}, {{ this.userCVC }}</p>
            <p>{{ this.cardNumber }}, {{ this.expiryMonth }}-{{ this.expiryYear }}, {{ this.cvc }}</p>
        
        </form>
    `,
    ViewModel: {
        amount: { default: 9.99 },
        
        userCardNumber: "string",
        get cardNumber() {
            return this.userCardNumber ? this.userCardNumber.replace(/-/g, ""): null;
        },
        get cardError() {
            if( this.cardNumber && !Stripe.card.validateCardNumber(this.cardNumber) ) {
                return "Invalid card number (ex: 4242-4242-4242).";
            }
        },

        userExpiry: "string",
        get expiryParts() {
            if(this.userExpiry) {
                return this.userExpiry.split("-").map(function(p){
                    return parseInt(p, 10);
                });
            }
        },
        get expiryMonth() {
            return this.expiryParts && this.expiryParts[0];
        },
        get expiryYear() {
            return this.expiryParts && this.expiryParts[1];
        },
        get expiryError() {
            if( (this.expiryMonth || this.expiryYear) &&
                 !Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) ) {
                return "Invalid expiration date (ex: 01-22).";
            }
        },

        userCVC: "string",
        get cvc() {
            return this.userCVC ?
                parseInt(this.userCVC, 10) : null;
        },
        get cvcError() {
            if( this.cvc && !Stripe.card.validateCVC(this.cvc) ) {
                return "Invalid CVC (ex: 123).";
            }
        },
        pay: function(event) {
            event.preventDefault();
    
            Stripe.card.createToken({
                number: this.cardNumber,
                cvc: this.cvc,
                exp_month: this.expiryMonth,
                exp_year: this.expiryYear
            }, function(status, response){
                if(status === 200) {
                    alert("Token: "+response.id);
                    // stripe.charges.create({
                    //   amount: this.amount,
                    //   currency: "usd",
                    //   description: "Example charge",
                    //   source: response.id,
                    // })
                } else {
                    alert("Error: "+response.error.message);
                }
            });
        }
    }
});

Validate the form

The problem

We need to show a warning message when information is entered incorrectly and disable the form until they have entered it correctly.

To do that, we’ll add the following properties to the ViewModel:

  • isCardValid - returns true if the card is valid
  • isCardInvalid - returns true if the card is invalid
  • errorMessage - returns the error for the first form value that has an error.

What you need to know

  • Use disabled:from to make an input disabled, like:

    <button disabled:from="isCardInvalid">...
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Stripe.setPublishableKey("pk_test_zCC2JrO3KSMeh7BB5x9OUe2U");

Component.extend({
    tag: 'cc-payment',
    view: `
        <form on:submit="this.pay(scope.event)">
            
            {{# if(this.errorMessage) }}
                <div class="message">{{ this.errorMessage }}</div>
            {{/ if }}

            <input type="text" name="number" placeholder="Card Number"
                    {{# if(this.cardError) }}class="is-error"{{/ if }}
                value:bind="this.userCardNumber"/>

            <input type="text" name="expiry" placeholder="MM-YY"
                {{# if(this.expiryError) }}class="is-error"{{/ if }}
                value:bind="this.userExpiry"/>

            <input type="text" name="cvc" placeholder="CVC"
            {{# if(this.cvcError) }}class="is-error"{{/ if }}
                value:bind="this.userCVC"/>

            <button disabled:from="this.isCardInvalid">Pay $\{{ this.amount }}</button>
        
        </form>
    `,
    ViewModel: {
        amount: { default: 9.99 },
        
        userCardNumber: "string",
        get cardNumber() {
            return this.userCardNumber ? this.userCardNumber.replace(/-/g, ""): null;
        },
        get cardError() {
            if( this.cardNumber && !Stripe.card.validateCardNumber(this.cardNumber) ) {
                return "Invalid card number (ex: 4242-4242-4242).";
            }
        },

        userExpiry: "string",
        get expiryParts() {
            if(this.userExpiry) {
                return this.userExpiry.split("-").map(function(p){
                    return parseInt(p, 10);
                });
            }
        },
        get expiryMonth() {
            return this.expiryParts && this.expiryParts[0];
        },
        get expiryYear() {
            return this.expiryParts && this.expiryParts[1];
        },
        get expiryError() {
            if( (this.expiryMonth || this.expiryYear) &&
                 !Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) ) {
                return "Invalid expiration date (ex: 01-22).";
            }
        },

        userCVC: "string",
        get cvc() {
            return this.userCVC ?
                parseInt(this.userCVC, 10) : null;
        },
        get cvcError() {
            if( this.cvc && !Stripe.card.validateCVC(this.cvc) ) {
                return "Invalid CVC (ex: 123).";
            }
        },
        pay: function(event) {
            event.preventDefault();
    
            Stripe.card.createToken({
                number: this.cardNumber,
                cvc: this.cvc,
                exp_month: this.expiryMonth,
                exp_year: this.expiryYear
            }, function(status, response){
                if(status === 200) {
                    alert("Token: "+response.id);
                    // stripe.charges.create({
                    //   amount: this.amount,
                    //   currency: "usd",
                    //   description: "Example charge",
                    //   source: response.id,
                    // })
                } else {
                    alert("Error: "+response.error.message);
                }
            });
        },

        get isCardValid() {
            return Stripe.card.validateCardNumber(this.cardNumber) &&
                Stripe.card.validateExpiry(this.expiryMonth, this.expiryYear) &&
                Stripe.card.validateCVC(this.cvc);
        },
        get isCardInvalid() {
            return !this.isCardValid;
        },
        get errorMessage() {
            return this.cardError || this.expiryError || this.cvcError;
        }
    }
});

Result

When complete, you should have a working credit card payment form like the following CodePen:

See the Pen Credit Card Guide (Simple) [Finished] by Bitovi (@bitovi) on CodePen.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 5.33.3.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news