< Back to blog

Create a Single-Page-App with Commerce.js & Vue.js

In this tutorial I'll show you how to build a custom, real-world, headless eCommerce store using Vue.js and Chec's JavaScript SDK, Commerce.js.

Live demo here Socks and Shoes Commerce.js

Let's get started!

Setting up Commerce.js & Vue.JS

Before beginning, let's use vue-cli to create the boilerplate for our Vue.js app; follow the instructions here.

Next, set up Commerce.js by installing the package via NPM with npm install @chec/commerce.js. Once installed create a file called configCommercejs.js within the root of our src folder (feel free to name the file whatever you'd like); within it, configure your Commerce client properly to fit your needs, authenticate using either a test or production COMMERCEJS_PUBLIC_KEY, and construct and export your instance.

Note: If utilizing any environment variable that's not NODE_ENV, BASE_APP you must prefix it with VUE_APP_ in order for it to be embedded into the client bundle (ultimate application). More information here.

Your configCommercejs.js file should look like this:

    import Commerce from '@chec/commerce.js';

    export default new Commerce(
      process.env.VUE_APP_COMMERCEJS_PUBLIC_KEY,
      process.env.NODE_ENV === 'development'
    );

State to make the SPA function

Your <App> container component will contain state reflecting all the products from your product inventory, the current cart in session, and an order object which will store a generated receipt object upon a successful checkout completion; all of these will have some initial (default) state.

From within your Cart/Checkout component, encapsulate some state such as;

  • a checkout object containing the generated checkout token
  • an errors object
  • a data properties holding values to the form input fields that will be used as constituent properties of the order object you'll be constructing and providing as an argument when completing the checkout process (capturing the order).

Setting up routes

Use vue-router to handle the routing in your single-page app. You can read more about setting up vue-router here. Within the root of your src directory, setup a route directory with an index.js file where you'll configure a vue-router instance that will take in an object defining and mapping your routes to the respective components.

Note: Export your router instance and inject it with the router option on our Vue app instance in src/main.js.

    import Vue from 'vue';
    import Router from 'vue-router';

    Vue.use(Router)

    export default new Router({
      mode: 'history',
      routes: [
        {
          path: "/",
          name: 'landing-page',
          component: () => import("@/components/LandingPage")
        },
        {
          path: "/products",
          name: 'products',
          component: () => import("@/components/Products"),
        },
        {
          path: "/cart-checkout",
          name: 'cart-checkout',
          component: () => import("@/components/CartCheckout"),
        },
        {
          path: "/thank-you",
          name: 'order-detail',
          component: () => import("@/components/ThankYou"),
        }
      ]
    })

Injecting Commerce.js into the Vue.js application

It's time to inject the Commerce.js functionality into your app by installing it as a plugin, this will allow for global level access. A provide/inject pattern will also work, but since you're only accessing the Commerce.js dependency from within two container components, this will do.

In your entry-point file, main.js, apply a mixin globally, allowing you to create a this.$commerce instance variable on every Vue instance's beforeCreate hook invocation. (Ideally this will be a packaged plugin passed via Vue.use(CommercejsPlugin).

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import commercejs from './configCommercejs'

    Vue.config.productionTip = false

    // inject commercejs as a plugin, globally
    Vue.mixin({
      beforeCreate() { 
            // Hook functions with the same name are merged 
            // into an array so that all of them will be called. Mixin hooks 
          // will be called before the component’s own hooks. 
          this.$commerce = commercejs
      }
    })

    new Vue({
      router, // inject router via ES6 object property value shorthand
      render: h => h(App),
    }).$mount('#app')

Create App container component

Now create your App component and add the necessary logic. Before writing any mark-up, add your initial state.

    data: function() {
        return {
          products: [],
          cart: null,
          order: null
        }
    }

Upon the mounting/creation of your App component, utilize Commerce.js to update your component's cart and products state. You're going to create two methods within the App component, retrieveCart and getAllProducts that are going to be invoked in the created hook.

The retrieveCart method will use this.$commerce.cart.retrieve to get the session's static cart object and set it to the application's this.cart state.

            // retrieve initial cart object
        retrieveCart() {
          this.$commerce.cart.retrieve().then(cart => {
            this.cart = cart
          }).catch(error => {
            // eslint-disable-next-line no-console
            console.log('There was an error retrieving the cart', error)
          });
        },

The getAllProducts method will be using the this.$commerce.products.list method to receive an array of static product objects and set it to this.products (you can read more about listing all products here.

Note: Below we're mapping each product, modifying the product object's variants array and changing each variant by adding an optionsById property which is an object mapping each of the respective options to a property key identifying the option.id. This allows for easier selecting of an option pertaining to a variant (you can view the Commerce.js product object here).

    // retrieve and list all products
    // https://commercejs.com/docs/api/#list-all-products
    getAllProducts() {
      this.$commerce.products.list().then(resp => {
        this.products = [
          ...resp.data.map(product => { // spread out product objects into array assigned to this.products
            return {
              ...product, // spread the current iteration's product's items on to the object returned
              variants: product.variants.map(variant => {
                return { // modify the shape of the product's variants mapping the options by id for easier selection
                  ...variant, 
                  optionsById: variant.options.reduce((obj, currentOption) => {
                    obj[currentOption.id] = {
                      ...currentOption,
                      variantId: variant.id
                    }
                    return obj;
                  }, {})
                }
              })
            }
          })
        ]
      })
      .catch(error => {
        // eslint-disable-next-line no-console
        console.log(error);
      });
    },

You also want to implement the addProductToCart , removeProductFromCart, and refreshCart methods within the App component; these methods will utilize the this.$commerce.cart methods add, update, and remove when invoked on respective events via [v-on](https://vuejs.org/v2/guide/events.html#ad) directives like @add-product-to-cart, @remove-product-from-cart, and @refresh-cart.

    // adds product to cart by invoking Commerce.js's Cart method commerce.cart.add
    // https://commercejs.com/docs/api/?javascript#add-item-to-cart
    addProductToCart({ productId, variant}) {
        if (this.cartAnimation) {
            this.cartAnimation = false // ensure cartAnimation flag is reset
        }
        this.$commerce.cart.add(
            productId,
            1,
            variant
        ).then(resp => {
            // if successful update cart and animate cart UI
            this.cartAnimation = true
            this.cart = resp.cart
        }).catch(error => {
            // eslint-disable-next-line no-console
            console.log(error)
        })
    },

    // removes product from cart by invoking Commerce.js's Cart method 'Cart.remove'
    // https://commercejs.com/docs/api/?javascript#remove-item-from-cart
    removeProductFromCart(lineItemId) {
        return this.$commerce.cart.remove(lineItemId).then((resp) => {
            this.cart = resp.cart
            return resp
        })
    },

    // refresh cart
    refreshCart() {
        this.$commerce.cart.refresh(() => {
            // successful
            this.cart = this.$commerce.cart.cart // TODO: when Commercejs.cart.refresh v2 resolves
            // with resp.cart object rather resp.cartId, assign with resp not with $commerce.cart.cart
        }).error(error => {
            // eslint-disable-next-line no-console
            console.log(error)
        })
    }

Now you can add markup to our <App> component. Let's begin by importing a [<Header> component](https://github.com/chec/example.checkout.v2/blob/vue.js/src/components/Header.vue), that will accept a cart prop to render the cart.total_items.

Go ahead and render a [router-view component from vue-router](https://router.vuejs.org/api/#router-view) to render the matched component on a given path. You want to pass all of your state to the matched component as props and apply v-on directives to listen to the events update:cart, update:order, add-product-to-cart, remove-product-from-cart, refresh-cart via the router-view.

Our <App> component should now look like this:

    <template>
    <div>
        <Header :cart="cart" :cartAnimation="cartAnimation" />
        <main id="main" class="flex">
          <router-view
            :products="products"
            :cart="cart"
            :order="order"
            @add-product-to-cart="addProductToCart"
            @remove-product-from-cart="removeProductFromCart"
            @refresh-cart="refreshCart"
            @update:order="order = $event"
            @update:cart="cart = $event"
          />
        </main>
        <footer class="footer flex pa4 bg-black-90 bg-red-m bg-green-l">
            <div class="self-end w-100">
              <p class="medium-text tc cherry">
                © 2019 CHEC PLATFORM/COMMERCEJS
              </p>
            </div>
        </footer>
    </div>

    </template>

    <script>
    import Header from './components/Header'
    import './styles/application.scss'
    export default {
      name: 'app',
      components: {
        Header
      },
      data: function() {
        return {
          products: [],
          cart: null,
          order: null,
          cartAnimation: false,
        }
      },
      created() {
        // invokes Commerce.js method commerce.cart.retrieve
        // and saves the initial Commercejs Cart Object to state/data this.cart
        this.retrieveCart()
        // invokes Commerce.js method commerce.products.list
        // and saves all the Products to this.products
        this.getAllProducts()
      },
      methods: {
        // retrieve initial cart object
        retrieveCart() {
          this.$commerce.cart.retrieve().then(cart => {
            this.cart = cart
          }).catch(error => {
            // eslint-disable-next-line no-console
            console.log('There was an error retrieving the cart', error)
          });
        },
        // retrieve and list all products
        // https://commercejs.com/docs/api/#list-all-products
        getAllProducts() {
          this.$commerce.products.list().then(
            (resp) => {
              this.products = [
                  ...resp.data.map(product => { // spread out product objects into array assigned to this.products
                    return {
                      ...product, // spread the current iteration's product's items on to the object returned
                      variants: product.variants.map(variant => {
                        return { // modify the shape of the product's variants mapping the options by id for easier selection
                          ...variant,
                          optionsById: variant.options.reduce((obj, currentOption) => {
                              obj[currentOption.id] = {
                                ...currentOption,
                                variantId: variant.id
                              }
                              return obj;
                          }, {})
                        }
                      })
                    }
                  })
                ]
            }
          ).catch(
            (error) => {
              // eslint-disable-next-line no-console
              console.log(error)
            }
          );
        },
        // adds product to cart by invoking Commerce.js's Cart method commerce.cart.add
        // https://commercejs.com/docs/api/?javascript#add-item-to-cart
        addProductToCart({ productId, variant}) {
          if (this.cartAnimation) {
            this.cartAnimation = false // ensure cartAnimation flag is reset
          }
          this.$commerce.cart.add(
            productId,
            '1',
            variant
          ).then(resp => {
            // if successful update cart and animate cart UI
            this.cartAnimation = true
            this.cart = resp.cart
          }).catch(error => {
            // eslint-disable-next-line no-console
            console.log(error)
          })
        },
        // removes product from cart by invoking Commerce.js's Cart method 'Cart.remove'
        // https://commercejs.com/docs/api/?javascript#remove-item-from-cart
        removeProductFromCart(lineItemId) {
          return this.$commerce.cart.remove(lineItemId).then((resp) => {
            this.cart = resp.cart
            return resp
          })
        },
        refreshCart() {
          this.$commerce.cart.refresh(() => {
            // successful
            this.cart = this.$commerce.cart.cart // TODO: when Commercejs.cart.refresh v2 resolves
            // with resp.cart object rather resp.cartId, assign with resp not with $commerce.cart.cart
          }).catch(error => {
            // eslint-disable-next-line no-console
            console.log(error)
          })
        }
      }
    }
    </script>

Creating the <Products> and <ProductDetail> components

Create a ProductDetail component to showcase an individual product, and a Products component to map and render ProductDetail components for all of your products in the state.

Within ProductDetail, accept a [product prop a Chec product which you will use to render the product.name, product.description, product.media.source for the image, product.price, and any product.variants. You're expecting both products in your Chec inventory to have size variants, so make sure you have a sizeSelect string in ProductDetail's state.

Implement the addProductToCart method which will emit a custom event add-product-to-cart passing the products ID, and any selected variant option (read more about emitting custom events in Vue). This event will ultimately be listened to in the root component, invoking App's addProductToCart method.

    data() {
        return {
            sizeSelect: ''
        }
    },
    props: ['product'],
    methods: {
        addProductToCart() {
            const product = {
                productId: this.product.id,
                variant: !this.product.variants.length || {
                    [this.product.variants[0].id]: this.sizeSelect
                }
            }
            this.$emit('add-product-to-cart', product)
        }
    }

Let's add the necessary mark-up to ProductDetail's template, rendering a <select> input if any product.variants are available. Your component should look like this:

    <template>
      <div v-if="product" class="productDetail w-100 pb1 ph3 ph4-ns">
        <div class="mw50rem center ph2">
          <div class="cf flex flex-column flex-row-l items-center">
            <div class="fl flex flex-column flex-grow-1 items-center justify-center w-100 w-50-l mt6-l order-1 order-0-l">
              <p class="large-title-text dark-gray w-100 ttl tl lh-solid mb1">
                {{product.name}}
              </p>
              <div
                class="medium-body-text gray w-100 tl"
                v-html=" product.description"
                ></div>
            </div>
            <div class="fl w-90 w-50-l self-start-l relative pb5 pa0-l">
              <img :src="product.media.source" alt="Product" width="100%" height="auto" />
              <div class="absolute absolute--fill flex justify-end items-end ml4">
                <div class="rotate-lift pr2">
                  <p class="medium-text ttu gray f6 tracked-mega-1 pb2 b">
                    type
                  </p>
                  <p class="large-title-text f1 fw9 ttu pl3">
                    brand
                  </p>
                </div>
              </div>
            </div>
          </div>
          <div class="productDetail__info-container center justify-start mw8 pb2 mt4 mt2-l ph0 ph1-ns">
            <div class="flex flex-row flex-grow-1 flex-wrap items-center">
              <Label
                labelTitle='price'
                :body='"$"+product.price.formatted_with_code'
                :classes="['mr4', 'mb3']"
              />
              <div class="relative" v-if="product.variants.length">
                <Label
                  placeholder="choose a size"
                  classes="chooseASize br1"
                  :body="product.variants[0].optionsById[sizeSelect] &&  product.variants[0].optionsById[sizeSelect].name">
                  <div class="arrowDownContainer ml2">
                    <TriangleIconSvg />
                  </div>
                  <select
                    class="absolute absolute--fill left-0 o-0 pointer w-100"
                    v-model="sizeSelect"
                    name='sizeSelect'
                  >
                    <option value="" disabled>Choose a size</option>
                    <option v-for="option of product.variants[0].options" :value="option.id" :key="option.id">
                      {{option.name}}
                    </option>
                  </select>
                </Label>
              </div>
            </div>
            <div class="w-100 w-50-l mt2 mt0-l">
              <button
                v-if="product.variants.length"
                :disabled="!!!sizeSelect"
                @click="addProductToCart"
                name="addToCartButton"
                class="button button__add-to-cart white ttu bg-dark-gray tracked-mega-1 w-100 mv3"
                :class="[sizeSelect ? 'dim' :'o-30']"
              >
                add to cart
              </button>
              <button
                v-else
                @click="addProductToCart"
                name="addToCartButton"
                class="button button__add-to-cart white ttu bg-dark-gray tracked-mega-1 w-100 mv3 dim"
              >
                add to cart
              </button>
            </div>
          </div>
        </div>
      </div>
      <p v-else>
        Loading Product
      </p>
    </template>

    <script>
    import Label from './Label'
    import TriangleIconSvg from '../assets/triangle-icon.svg'
    export default {
      name: 'ProductDetail',
      props: ['product'],
      components: {
        Label,
        TriangleIconSvg
      },
      methods: {
        addProductToCart() {
          const product = {
            productId: this.product.id,
            variant: !this.product.variants.length || {
              [this.product.variants[0].id]: this.sizeSelect
            }
          }
          this.$emit('add-product-to-cart', product)
        }
      },
      data() {
        return {
          sizeSelect: ''
        }
      }
    }
    </script>

Next, within the Products component, accept a products property that by default is an empty array. This component will render a <ProductDetail> component for each product in the products property.

Note: We're listening to the add-product-to-cart event on the <ProductDetail> component continuing emitting (bubbling) it up.

The <App> container component will eventually listen to the and handle the event by utilizing this.$commerce.cart.add to make a request to update the cart then reflect the updated cart data in the state if added successfully.

This is also using a computed method to reverse the ordering of the products property to display the latest items first. (read more about computed properties here).

Your ProductDetail component should like this:

    <template>
      <div class="w-100">
        <template v-if="products.length">
          <ProductDetail
            v-for="(product, id) in productsReversed" :key="id"
            @add-product-to-cart="$emit('add-product-to-cart', $event)"
            :product="product"
          />
        </template>
        <FootPrintsLoading v-else />
      </div>
    </template>
    <script>
    import FootPrintsLoading from './FootPrintsLoading';
    import ProductDetail from './ProductDetail';
    export default {
      name: "AllProducts",
      props: {
        products: {
          type: Array,
          default: () => []
        }
      },
      computed: {
        productsReversed() {
          return [...this.products].reverse()
        }
      },
      components: {
        ProductDetail,
        FootPrintsLoading
      }
    }
    </script>

Let's begin creating the cart/checkout related components

Create <CartCheckout> and <CartLineItem> components

The <CartCheckout> component will act as a hybrid component representing the cart and checkout interfaces. This allows the UI to leverage Commerce.js's ability to update your cart—such as adding a new cart item or updating the quantity of a cart item—even after generating a checkout token as long as we have not captured an order yet. This can be helpful for things such as up-selling during a checkout.

Note: The state in your app such as cart, products, and order are representations of the responses from the Chec API via the Commerce.js SDK. This state should always correspond with some sort of Commerce.js response—in other words it shouldn't usually be updated manually.

Let's begin by creating the <CartLineItem> component that will represent a single cart-item. This component will handle emitting events to trigger cart item actions like remove-product-from-cart and update-quantity (you can see what a cart-item object looks like by viewing the cart object here). The <CartCheckout> container component will listen to these events and invoke the respective methods.

<CartLineItem> :

    <template>
      <div class="">
        <div class="flex flex-row justify-between items-center ph4 pv2">
          <button
            @click="removeProductFromCart"
            class="cartIconContainer dim pointer pa0 bg-none">
            <RemoveIconSvg width="100%" height="auto" />
          </button>
          <div class="w-25">
            <div
               class="aspect-ratio aspect-ratio--1x1"
               :style="{
                 backgroundRepeat: 'no-repeat',
                 backgroundPosition: 'center',
                 backgroundSize: 'contain',
                 backgroundImage: `url(${/\b(\w*sock(s)?\w*)\b/.test(item.name.trim('').toLowerCase()) ? require('../assets/updated-sock-image.png') : require('../assets/pair-shoes-small.png')})` // checks if regex matches socks or sock
                 // in order to render proper cart line item image. ideally we would have a media
                 // property in the cart line-item objects
               }"
             />
          </div>
          <p class="medium-text f6 white tr ttu mw4 lh-title">
            {{item.name}}
            <span v-if="item.variants.length" class="db f7 pt1">
              {{ item.variants[0].option_name }}
            </span>
            <span class="db f7 pt1">
              ${{item.line_total.formatted_with_code}}
            </span>
            <span class="db">
              <div class="flex flex-row items-center justify-end">
                <button class="bg-none white f4 pointer grow dim ph2" @click="() => updateQuantity(item.quantity - 1)">-</button>
                <span class="ttl">x</span>{{item.quantity}}
                <button class="bg-none white f5 pointer grow dim ph2" @click="() => updateQuantity(item.quantity + 1)">+</button>
              </div>
            </span>
          </p>
        </div>
      </div>
    </template>
    <script>
    import RemoveIconSvg from '../assets/remove-icon.svg'
    export default {
      name: "CartLineItem",
      props: ["item"],
      components: {
        RemoveIconSvg
      },
      methods: {
        removeProductFromCart() {
          this.$emit('remove-product-from-cart', this.item.id)
        },
        updateQuantity(quantity) {
          this.$emit('update-quantity', this.item.id, quantity)
        }
      }
    }
    </script>

The <CartCheckout> component will act as a container component as it will have some state corresponding to the <form> collecting the required information to capture an order. It will also have some methods such as getShippingOptions, getRegions, getAllCountries, updateQuantity createCheckout.

Upon the creation of the component, invoke the getAllCountries and getRegions methods to set-up initial state, this can be done within the created lifecycle hook. When calling getRegions we'll be passing this.deliveryCountry as an argument - by default this is set to 'US', as this.deliveryCount is .

    created() {
        this.getAllCountries()
        this.getRegions(this.deliveryCountry)
    }

You'll be implementing custom watch methods to react to state changes and make some necessary updates in response to the change. Watchers allow you to hook into reactive data changes within the component, making it easy to initiate side-effects.

    watch: {
        cart(newCart, oldCart) {
          if (newCart !== oldCart) {
            // since this is a hyrbid component showcasing the cart and checkout simultaneously
            // we want to watch for this.cart updates, and do something such as
            // resetting the checkout state when there are no longer any cart items
            // Also if there was a checkout token object, this.checkout, 
                    // initiated prior to the change
            // then we also want to get an updated checkout token object
            // from Chec by calling this.createCheckout() for the new cart
            if (newCart.total_items === 0) { // cart changed the
              this.checkout = null // clear checkout token object if cart empty now
              alert("You must add items to your cart to continue checkout")
              return;
            }
            // only invoke createCheckout if this.checkout was initiated prior to this update to get an updated checkout token object
            if (this.checkout) {
              this.createCheckout()
            }
          }
        },
        deliveryCountry(newVal) { // do something when new delivery country is selected
            this.getRegions(newVal) // update the regions/provinces/states that are based on the selected country (this.deliveryCountry)
          if (this.checkout) { // check if we had a checkout option prior
            // the shipping options based on the selected country
            this.getShippingOptions(this.checkout.id, newVal)
          }
        }
      }

Now, set-up the state data for <CartCheckout> that will be uses to bind to respective form input values via v-model. You'll be mapping the param key from Commerce.js validation error objects to map them to the relevant input fields via the errors in the data.

    data() {
        return {
          firstName: 'John',
          lastName: 'Doe',
          "customer[email]": '[email protected]',
          "shipping[name]": 'John Doe',
          "shipping[street]": '1161 Mission St',
          "shipping[town_city]": 'San Francisco',
          deliveryState: 'CA',
          "shipping[postal_zip_code]": "94103",
          deliveryCountry: 'US', // selected country e.g this.countries[this.deliveryCountry]
          countries: {},
          subdivisions: {},
          checkout: null,

          // state below is set after checkout token is generated
          "fulfillment[shipping_method]": '',
          shippingOptions: [],
          shippingOptionsById: {},
          cardNumber: '',
          expMonth: '01',
          expYear: '2021',
          cvc: '123',
          billingPostalZipcode: '94103',

          errors: {
            "fulfillment[shipping_method]": null,
            gateway_error: null,
            "customer[email]": null,
            "shipping[name]": null,
            "shipping[street]": null,
            "shipping[town_city]": null,
            "shipping[postal_zip_code]": null
          },
        }
      } 

Now create the mark-up that will react to the data above changing.

    <template>
      <div v-if="cart" class="flex flex-grow-1 flex-column bg-tan-white w-100 pb4">
        <div class="flex justify-between mw9 w-100 items-center center pt4 ph4">
          <router-link to="/products" class="flex items-center medium-text f6 tracked-mega ttu no-underline dark-gray dim lh-solid">
            <div class="arrowIconContainer fill-cherry pr4">
              <ArrowIconSvg />
            </div>
            continue shopping
          </router-link>

          <p class="medium-text f6 tracked-mega ttu dark-gray tracked-mega lh-solid tr">
            {{ cart ? cart.total_items : '0' }}
            <span class="f7">{{cart ? (cart.total_items === 1 ? 'item' : 'items') : 'items'}}</span>
          </p>
        </div>
        <div class="cf mw9 center w-100 ph3 mt5">
            <div class="fl w-100 w-40-l ph2 ph4-l mb4">
                <div class="relative z-1 br3 bg-dark-gray w-100 shadow-3 pv4 overflow-scroll">
                  <CartLineItem
                    v-for="item in cart.line_items"
                    @remove-product-from-cart="removeProductFromCart"
                    :item="item"
                    :key="item.id"
                    @update-quantity="updateQuantity"
                  />
                </div>
                <div class="pt4 pb3 nt3 br3 ph4 bg-cherry">
                  <div class="flex pb1 justify-between items-center w-100 medium-text f6 white ttu b tracked-mega-1">
                    <p>
                      subtotal
                    </p>
                    <p class="tr lh-title">
                      {{cart ? cart.subtotal.formatted_with_code : '----'}}
                    </p>
                  </div>
                </div>
            </div>
            <div class="fl w-100 w-60-l ph2 ph4-l">
              <form class="font-roboto mb4 ttu f6 tracked-mega light-gray">
                  <div class="flex justify-between">
                    <div class="w-50 pr2 flex flex-column">
                      <label>
                        <p class="checkoutFormInputLabel">
                          first name
                        </p>
                      </label>
                      <input
                        class="checkoutFormInput"
                        type="text"
                        name="firstName"
                        v-model="firstName"
                        placeholder="first name"
                      />
                    </div>
                    <div class="w-50 pl2 flex flex-column">
                      <label>
                        <p class="checkoutFormInputLabel">
                          first name
                        </p>
                      </label>
                      <input
                        class="checkoutFormInput"
                        type="text"
                        name="lastName"
                        v-model="lastName"
                        placeholder="first name"
                      />
                    </div>
                  </div>

                  <div>
                    <label>
                      <p class="checkoutFormInputLabel">
                        email
                      </p>
                    </label>
                    <input
                      class="checkoutFormInput"
                      :class="[errors['customer[email]'] && 'input-error']"
                      type="email"
                      name="customer[email]"
                      v-model="$data['customer[email]']"
                      placeholder="Email Address"
                    />
                  </div>
                  <div>
                    <label>
                      <p class="checkoutFormInputLabel">
                        Delivery Name
                      </p>
                    </label>
                    <input
                      class="checkoutFormInput"
                      :class="[errors['shipping[name]'] && 'input-error']"
                      type="text"
                      name="shipping[name]"
                      v-model="$data['shipping[name]']"
                      placeholder="Delivery Name"
                    />
                  </div>
                  <div>
                    <label>
                      <p class="checkoutFormInputLabel">
                        delivery street address
                      </p>
                    </label>
                    <input
                      class="checkoutFormInput"
                      :class="[errors['shipping[street]'] && 'input-error']"
                      type="text"
                      name="shipping[street]"
                      v-model="$data['shipping[street]']"
                      placeholder="Delivery Street Address"
                    />
                  </div>
                  <div class="flex justify-between">
                    <div class="w-70 pr2 flex flex-column">
                      <label>
                        <p class="checkoutFormInputLabel">
                          city
                        </p>
                      </label>
                      <input
                        class="checkoutFormInput"
                        :class="[errors['shipping[town_city]'] && 'input-error']"
                        type="text"
                        name="shipping[town_city]"
                        v-model="$data['shipping[town_city]']"
                        placeholder="City"
                      />
                    </div>
                    <div class="w-30 pl2 flex flex-column">
                      <label>
                        <p class="checkoutFormInputLabel">
                          post/zip code
                        </p>
                      </label>
                      <input
                        class="checkoutFormInput"
                        :class="[errors['shipping[postal_zip_code]'] && 'input-error']"
                        type="number"
                        name="shipping[postal_zip_code]"
                        v-model="$data['shipping[postal_zip_code']"
                        placeholder="post/zip code"
                      />
                    </div>
                  </div>
                  <div class="flex justify-between">
                    <div class="w-50 pr2 flex flex-column">
                      <label>
                        <p class="checkoutFormInputLabel">
                          country
                        </p>
                      </label>
                      <div class="checkoutFormInput flex-grow-1 relative">
                        <p>
                          {{countries[deliveryCountry] || 'Select your country'}}
                        </p>
                        <select
                          name="deliveryCountry"
                          v-model="deliveryCountry"
                          placeholder="Delivery"
                          class="absolute absolute--fill left-0 o-0 pointer w-100">
                          <option value="" disabled>Select your country</option>
                          <option v-for="(countryValue, countryKey) in countries" :value="countryKey" :key="countryKey">
                            {{ countryValue }}
                          </option>
                        </select>
                      </div>
                    </div>
                    <div class="w-50 pl2 flex flex-column relative">
                      <label>
                        <p class="checkoutFormInputLabel">
                          state/province/region
                        </p>
                      </label>
                      <div class="checkoutFormInput flex-grow-1 relative">
                        <p>
                          {{deliveryCountry ? subdivisions[deliveryState] || 'Select your state' : 'Select a country first'}}
                        </p>
                        <select
                          name="deliveryState"
                          :disabled="!!!deliveryCountry"
                          v-model="deliveryState"
                          class="absolute absolute--fill left-0 o-0 pointer w-100">
                          <option value="" disabled>Select your state</option>
                          <option v-for="(subdivisionValue, subdivisionKey) in subdivisions" :value="subdivisionKey" :key="subdivisionKey">
                            {{ subdivisions[subdivisionKey] }}
                          </option>
                        </select>
                      </div>
                    </div>
                  </div>
                  <div v-if="checkout">
                    <div class="w-100 flex flex-column mt4">
                      <label>
                        <p class="checkoutFormInputLabel">
                          delivery method
                        </p>
                      </label>
                      <div
                        class="checkoutFormInput flex-grow-1 relative"
                        :class="[errors['fulfillment[shipping_method]'] && 'input-error']">
                        <p>
                          {{
                            $data['fulfillment[shipping_method]'] ?
                            `${
                              shippingOptionsById[this["fulfillment[shipping_method]"]].description}
                              - $${shippingOptionsById[this["fulfillment[shipping_method]"]].price.formatted_with_code
                            }` :
                            'Select a delivery method'
                          }}
                        </p>
                        <select
                          name="fulfillment[shipping_method]"
                          v-model="$data['fulfillment[shipping_method]']"
                          placeholder="Shipping Option"
                          class="absolute absolute--fill left-0 o-0 pointer w-100">
                          <option value="" disabled>Select a delivery method</option>
                          <option v-for="option in shippingOptions" :value="option.id" :key="option.id">
                            {{ `${option.description} - $${option.price.formatted_with_code}` }}
                          </option>
                        </select>
                      </div>
                    </div>
                    <div
                      class="w-100 flex flex-column">
                      <label>
                        <p class="checkoutFormInputLabel">
                          card number
                        </p>
                      </label>
                      <input
                        class="checkoutFormInput"
                        :class="[errors.gateway_error && 'input-error']"
                        type="number"
                        name="cardNumber"
                        v-model="cardNumber"
                        placeholder="Card Number"
                      />
                    </div>
                    <div class="w-100 flex">
                      <div class="w-third flex flex-column">
                        <label>
                          <p class="checkoutFormInputLabel">
                            expiry month
                          </p>
                        </label>
                        <input
                          class="checkoutFormInput"
                          type="number"
                          name="expMonth"
                          v-model="expMonth"
                          placeholder="expiry month"
                        />
                      </div>
                      <div class="w-third flex flex-column ph2">
                        <label>
                          <p class="checkoutFormInputLabel">
                            expiry year
                          </p>
                        </label>
                        <input
                          class="checkoutFormInput"
                          type="number"
                          name="expYear"
                          v-model="expYear"
                          placeholder="expiry year (yyyy)"
                        />
                      </div>
                      <div class="w-third flex flex-column ph2">
                        <label>
                          <p class="checkoutFormInputLabel">
                            cvc
                          </p>
                        </label>
                        <input
                          class="checkoutFormInput"
                          type="number"
                          name="cvc"
                          v-model="cvc"
                          placeholder="cvc"
                        />
                      </div>
                    </div>
                  </div>
                  <div class="flex flex-column">
                    <button
                      @click.prevent="() => checkout ? captureOrder() : createCheckout()"
                      class="button__checkout bg-cherry white ttu b self-end pointer dim shadow-5 tracked-mega-1"
                    >
                      {{`${checkout ? 'buy now' : 'delivery & payment'}`}}
                    </button>
                  </div>
                </form>
            </div>
        </div>
      </div>
      <p v-else>
        Loading...
      </p>
    </template>

In the template we're rendering a cart element using the cart data and checkout form to collect the relevant information, and also to reflect errors appropriately.

The checkout flow when working with Commerce.js operates in a flow that begins with a cart or product generating a checkout token object. Data from the checkout token object like id along with other information like all the line-items––if the checkout token object was generated using a cart id––customer email, type of payment gateway and respective information is used to capture an order, if done successfully an receipt object will be returned.

On a successful capturing of an order, will reflect the receipt object within the <ThankYou> component by emitting the update:order event along with the receipt object and redirecting to the /thank-you path.

Take note that if wanting to retrieve an order again, it is only possible with a PRIVATE_KEY as an X-Authorization header through a POST request to orders endpoint.

That's it, you're all done; you can find the official repo here. You can read about deploying a vue-cli generated Vue app here.

Resources and Community

Check out our documentation and community articles.

Join our community on Slack to connect with eCommerce developers from around the world. Share your projects with us, ask questions, or just hang out!

Join our community
Dev community image