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
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!
