If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTo finish checkout, we need to submit this form via AJAX to an API endpoint. Copy the URL and open up a new tab to go to /api
: the home of our API documentation.
One API resource in here is called Purchase
. This is what we will be using for checkout: we'll send a POST request to /api/purchases
to create a new "purchase". The fields it needs are pretty familiar: the six customer fields that we have in our form plus a purchaseItems
array field. Each item in this array will be an object with product
, color
and quantity
keys: the exact structure of the items in our cart
data.
To help us talk to this endpoint, in assets/services/
, create a new file called checkout-service.js
Inside, I'll paste a function... which is only about one line long. We import axios
, export a function called createOrder
, which takes in that data structure we discussed, and posts to /api/purchases
.
import axios from 'axios'; | |
/** | |
* Makes a POST call to create a purchase object after checkout | |
* | |
* @param {Object} data | |
* @return {Promise} | |
*/ | |
export function createOrder(data) { | |
return axios.post('/api/purchases', data); | |
} |
Cool! Let's go use this! In the checkout form component - index.vue
- after the last field, I'll paste in a submit button. Make sure to paste this outside of the form-col
div - I just pasted it inside... and I'll regret it in a few minutes.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
... lines 5 - 45 | |
<div class="form-row justify-content-end align-items-center"> | |
<loading v-show="loading" /> | |
<div class="col-auto"> | |
<button | |
type="submit" | |
class="btn btn-info btn-lg" | |
> | |
Order! | |
</button> | |
</div> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 62 - 105 |
Anyways, there's nothing special about this: just <button type="submit">
.
We're also going to need a loading animation while this is saving. So down in data
, add a new key: loading: false
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 66 | |
export default { | |
name: 'CheckoutForm', | |
... lines 69 - 72 | |
data() { | |
return { | |
... lines 75 - 83 | |
loading: false, | |
}; | |
}, | |
... lines 87 - 102 | |
}; | |
</script> |
Then pull in our trusty loading component: import Loading
from @/components/loading
. Add that to components
:
... lines 1 - 62 | |
<script> | |
import FormInput from '@/components/checkout/form-input'; | |
import Loading from '@/components/loading'; | |
export default { | |
name: 'CheckoutForm', | |
components: { | |
FormInput, | |
Loading, | |
}, | |
data() { | |
return { | |
... lines 75 - 83 | |
loading: false, | |
}; | |
}, | |
... lines 87 - 102 | |
}; | |
</script> |
... and up in the template, inside that new div, say <loading/>
with v-show="loading"
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
... lines 5 - 45 | |
<div class="form-row justify-content-end align-items-center"> | |
<loading v-show="loading" /> | |
... lines 48 - 56 | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 62 - 105 |
Our next step is pretty straightforward: we need to add a method that, when the form submits, sends the Ajax request. Down under methods
add a new one called async onSubmit()
. I'm immediately making this async
because I know I'm going to want to await
for the AJAX call to finish. Start with this.loading = true
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 67 | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 87 | |
methods: { | |
... lines 89 - 102 | |
async onSubmit() { | |
this.loading = true; | |
... lines 105 - 117 | |
}, | |
}, | |
}; | |
</script> |
Because this is a checkout form, I don't want anything weird to happen. And so, I'm going to write more error handling than we've done so far. Add a try {} catch
and a finally
. Inside that last section, set this.loading = false
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 67 | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 87 | |
methods: { | |
... lines 89 - 102 | |
async onSubmit() { | |
this.loading = true; | |
try { | |
... lines 107 - 112 | |
} catch (error) { | |
... line 114 | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
Thanks to this - whether we're successful or not - the loading animation will hide.
Inside try
, say const response =
await createOrder()
. Make sure you hit tab so that PhpStorm adds the import on top.
... lines 1 - 62 | |
<script> | |
... lines 64 - 65 | |
import { createOrder } from '@/services/checkout-service'; | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 87 | |
methods: { | |
... lines 89 - 102 | |
async onSubmit() { | |
this.loading = true; | |
try { | |
const response = await createOrder({ | |
... lines 108 - 109 | |
}); | |
... lines 111 - 112 | |
} catch (error) { | |
... line 114 | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
For the data argument, we need to pass the individual form fields and an extra purchaseItems
field. The form fields are stored on the form
data. So what we can do down here is say ...this.form
. That will expand the object so that the data will have those 6 keys.
... lines 1 - 62 | |
<script> | |
... lines 64 - 65 | |
import { createOrder } from '@/services/checkout-service'; | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 87 | |
methods: { | |
... lines 89 - 102 | |
async onSubmit() { | |
this.loading = true; | |
try { | |
const response = await createOrder({ | |
...this.form, | |
... line 109 | |
}); | |
... lines 111 - 112 | |
} catch (error) { | |
... line 114 | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
The last field we need to send is purchaseItems
. For now, set that to an empty array.
... lines 1 - 62 | |
<script> | |
... lines 64 - 65 | |
import { createOrder } from '@/services/checkout-service'; | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 87 | |
methods: { | |
... lines 89 - 102 | |
async onSubmit() { | |
this.loading = true; | |
try { | |
const response = await createOrder({ | |
...this.form, | |
purchaseItems: [], | |
}); | |
... lines 111 - 112 | |
} catch (error) { | |
... line 114 | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
Down in catch, use console.error()
- that's like console.log()
... but will look red - to log error.response
. In Axios, if an AJAX call fails, it stores the error response on this key. Oh, and also console.log()
the success response up in try
.... actually response.data
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 65 | |
import { createOrder } from '@/services/checkout-service'; | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 87 | |
methods: { | |
... lines 89 - 102 | |
async onSubmit() { | |
this.loading = true; | |
try { | |
const response = await createOrder({ | |
...this.form, | |
purchaseItems: [], | |
}); | |
console.log(response.data); | |
} catch (error) { | |
console.error(error.response); | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
Awesome! We're eventually going to do something on success and error, but this is a good start.
Let's fill in the empty purchaseItems
. Go back to the Vue dev tools and click on the ShoppingCart
component. Not by accident... because our API is fairly consistent, the purchaseItems
key that we need to send matches the cart.items
data exactly, where each item has color
, product
and quantity
.
This means that we need access to the cart
object inside our checkout form. Over in index.vue
, we don't have access to that yet, so let's add a new prop so it can be passed in. Add props
, then cart
with type: Object
and required: true
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 67 | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 73 | |
props: { | |
cart: { | |
type: Object, | |
required: true, | |
}, | |
}, | |
... lines 80 - 125 | |
}; | |
</script> |
Before we use that, open shopping-cart.vue
. This is the component that renders the CheckoutForm
component: On <checkout-form
, pass cart="cart"
. I mean :cart=cart
.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 29 | |
<div class="content p-3"> | |
... lines 31 - 32 | |
<transition | |
... lines 34 - 35 | |
> | |
... lines 37 - 46 | |
<checkout-form | |
v-if="completeCart && currentState === 'checkout'" | |
:cart="cart" | |
/> | |
</transition> | |
... lines 52 - 62 | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 68 - 184 |
Back over in index.vue
, now that the cart
prop exists, use it: set purchaseItems
to this.cart.items
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 67 | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 93 | |
methods: { | |
... lines 95 - 108 | |
async onSubmit() { | |
... lines 110 - 112 | |
const response = await createOrder({ | |
... line 114 | |
purchaseItems: this.cart.items, | |
}); | |
... lines 117 - 118 | |
} catch (error) { | |
console.error(error.response); | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
Ok! Now that this method is... kind of done, let's hook it up! On submit of the form, we want Vue to call it. Up on the <form>
tag... here it is - add @submit="onSubmit"
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form @submit="onSubmit"> | |
... lines 5 - 57 | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 62 - 128 |
Testing time! Move over and click "check out". Uh... that button is not in the right place. Come on Ryan!
Move the button div outside of the column. Now... much better.
Ok: submit the empty form. It... kinda looked like it worked? I saw the loading animation... and then the page refreshed. Duh. I forgot to prevent the form from its "default" behavior.
The easiest way to fix this is down inside the onSubmit
method, this will receive an event
argument... and then we can say event.preventDefault()
.
... lines 1 - 62 | |
<script> | |
... lines 64 - 67 | |
export default { | |
name: 'CheckoutForm', | |
... lines 70 - 93 | |
methods: { | |
... lines 95 - 108 | |
async onSubmit(event) { | |
event.preventDefault(); | |
... lines 111 - 124 | |
}, | |
}, | |
}; | |
</script> |
Easy peasy. Or... we can use the fancy Vue way! Remove all of that... then go up to the form element where we call this. Change this to @submit.prevent
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form @submit.prevent="onSubmit"> | |
... lines 5 - 57 | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 62 - 128 |
Oooo. That .prevent
is one of a small number of "event modifiers". This says, "prevent the default". There are others like .stop
and .once
that are less commonly used.
Let's try it again! Go to check out and... awesome! I saw the loading animation for just a moment and then it went away. Go check the console. Woo! A 400 bad request and a console.error()
of the response object.
Why did our AJAX call fail? Because our API already has built-in validation rules. Next: let's handle when the AJAX call fails: both to render a message if there is an unexpected error and, after, to render error messages next to each field that fails validation.
"Houston: no signs of life"
Start the conversation!
// package.json
{
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
"@symfony/webpack-encore": "^0.30.0", // 0.30.2
"axios": "^0.19.2", // 0.19.2
"bootstrap": "^4.4.1", // 4.5.3
"core-js": "^3.0.0", // 3.6.5
"eslint": "^6.7.2", // 6.8.0
"eslint-config-airbnb-base": "^14.0.0", // 14.2.0
"eslint-plugin-import": "^2.19.1", // 2.22.1
"eslint-plugin-vue": "^6.0.1", // 6.2.2
"regenerator-runtime": "^0.13.2", // 0.13.7
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^8.0.0", // 8.0.2
"vue": "^2.6.11", // 2.6.12
"vue-loader": "^15.9.1", // 15.9.4
"vue-template-compiler": "^2.6.11", // 2.6.12
"webpack-notifier": "^1.6.0" // 1.8.0
}
}