Integrating Stripe in Angular With Stripe Elements

Stripe recently released Stripe Elements, a set of UI elements that make it easy to build a custom checkout flow, complete with realtime validation and autocomplete support. In this post we’ll go over the basics of integrating Stripe and Stripe Elements on the frontend with Angular.

We’ll create a simple payment form that gets a token from the Stripe API. Stripe’s documentation for Elements is for integrating using vanilla JavaScript, but here we’ll modify this implementation slightly to integrate with an Angular template-driven form.

Setup

First, in your project’s index.html file, you’ll want to add and initialize Stripe.js as well as initialize Stripe Elements:

index.html

...
<body>
  <app-root></app-root>

  <script src="https://js.stripe.com/v3/"></script>
  <script type="text/javascript">
    var stripe = Stripe('pk_test_XXXXXXXXXXXXXXXXX'); // use your test publishable key
    var elements = stripe.elements();
  </script>
</body>
</html>

Remember to change to your live publishable key once your app goes to production.

Since Stripe.js is added outside the scope of the project and doesn’t have typings, TypeScript would normally complain when trying to access stripe or elements. To fix this, we’ll add two declarations to the project’s typings.d.ts file:

typings.d.ts

// ...

declare var stripe: any;
declare var elements: any;

We’ll be using Angular’s template-driven forms for our simple payment form, so we also have to import the FormsModule in our app or feature module:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

import {FormsModule} from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Usage

The template markup for a basic checkout flow is as simple as it gets:

app.component.html

<form #checkout="ngForm" (ngSubmit)="onSubmit(checkout)" class="checkout">
  <div class="form-row">
    <label for="card-info">Card Info</label>
    <div id="card-info" #cardInfo></div>

    <div id="card-errors" role="alert" *ngIf="error">{{ error }}</div>
  </div>

  <button type="submit">Pay $777</button>
</form>

The #card-info element will be the container for the Stripe Elements, and we also created a container div to display error messages, if any.


The fun part starts when we hook everything up in the component class. Here’s the code to make our example work, with some interesting parts highlighted:

app.components.ts

import {
  Component,
  AfterViewInit,
  OnDestroy,
  ViewChild,
  ElementRef,
  ChangeDetectorRef
} from '@angular/core';

import { NgForm } from '@angular/forms';

@Component({ ... })
export class AppComponent implements AfterViewInit, OnDestroy {
  @ViewChild('cardInfo') cardInfo: ElementRef;

  card: any;
  cardHandler = this.onChange.bind(this);
  error: string;

  constructor(private cd: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.card = elements.create('card');
    this.card.mount(this.cardInfo.nativeElement);

    this.card.addEventListener('change', this.cardHandler);
  }

  ngOnDestroy() {
    this.card.removeEventListener('change', this.cardHandler);
    this.card.destroy();
  }

  onChange({ error }) {
    if (error) {
      this.error = error.message;
    } else {
      this.error = null;
    }
    this.cd.detectChanges();
  }

  async onSubmit(form: NgForm) {
    const { token, error } = await stripe.createToken(this.card);

    if (error) {
      console.log('Something is wrong:', error);
    } else {
      console.log('Success!', token);
      // ...send the token to the your backend to process the charge
    }
  }
}

There may seem to be a lot doing on at first glance, but it’s all really straightforward. Here are a few things to note:

  • We get access to the container for the card element using the ViewChild decorator.
  • We bind our onChange method to the this of the class and save the new reference as cardHandler. This reference is used to add an event listener when the card element is created and remove it in the OnDestroy hook.
  • We initialize the card element in the AfterViewInit lifecycle hook, to ensure that our container element is available.
  • We make use of ChangeDetectorRef to manually instruct Angular to run a change detection cycle in the onChange method.
  • Our form submission method, onSubmit, is an async function that awaits for Stripe’s createToken promise to resolve.
  • Once we get a valid token back from Stripe, the token should be sent to your backend or to a cloud function to process the charge. Checkout this video for an example.
  • In the OnDestroy we clean up by removing the change event listener and destroying the card element.

Our example is really barebones, but in a real app you’ll want to also implement a simple boolean flag that prevents the user from submitting the form multiple times in a row. For example, the submit button could be replaced by a loading indicator from the time the form is submitted up until your backend sends a success message indicating that the charge was processed. In that case, you’d redirect the user to something like a confirmation page.

To test things out, use card number 4242 4242 4242 4242 with any expiration date in the future, any 3-digit number for the CVC and any valid zip code.

Styling

We have a simple checkout form working, but it looks pretty dull. Let’s add a touch of styles. First, let’s style our form and submit button:

app.component.css

form.checkout {
  max-width: 500px;
  margin: 2rem auto;
  text-align: center;
  border: 2px solid #eee;
  border-radius: 8px;
  padding: 1rem 2rem;
  background: white;

  font-family: monospace;
  color: #525252;
  font-size: 1.1rem;
}

form.checkout button {
  padding: 0.5rem 1rem;
  color: white;
  background: coral;
  border: none;
  border-radius: 4px;
  margin-top: 1rem;
}

form.checkout button:active {
  background: rgb(165, 76, 43);
}

The Stripe card element itself can be styled using selectors like .StripeElement, .StripeElement–focus and .StripeElement–invalid. There are a few ready-made theme examples available, but here we’ll just add the default style provided by Stripe:

...

.StripeElement {
  margin: 1rem 0 1rem;
  background-color: white;
  padding: 8px 12px;
  border-radius: 4px;
  border: 1px solid transparent;
  box-shadow: 0 1px 3px 0 #e6ebf1;
  -webkit-transition: box-shadow 150ms ease;
  transition: box-shadow 150ms ease;
}

.StripeElement--focus {
  box-shadow: 0 1px 3px 0 #cfd7df;
}

.StripeElement--invalid {
  border-color: #fa755a;
}

.StripeElement--webkit-autofill {
  background-color: #fefde5 !important;
}

Some basic styles can also be passed-in with an option object as a second argument to the create method:

app.component.ts (partial)

ngAfterViewInit() {
  const style = {
    base: {
      lineHeight: '24px',
      fontFamily: 'monospace',
      fontSmoothing: 'antialiased',
      fontSize: '19px',
      '::placeholder': {
        color: 'purple'
      }
    }
  };

  this.card = elements.create('card', { style });
  this.card.mount(this.cardInfo.nativeElement);

  this.card.addEventListener('change', this.cardHandler);
}

You can refer to the options API reference for a list of all the possible style configuration options.

This is what our form looks like...

Example of Stripe Elements in Angular

Additional Fields

Our checkout form is all well and good, but what if we also want save some additional data for the customer in Stripe? It’s as simple as passing a second argument to createToken with any extra field.

Here for example we also send the email address for the customer:

app.component.ts

async onSubmit(form: NgForm) {
  const { token, error } = await stripe.createToken(this.card, {
    email: this.emailAddress
  });

  // ...
}
  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...