Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Checkout Submit

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

To 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.

POST /api/purchases

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);
}

Adding the "Order" Button

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

Sending the AJAX Call

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.

Grabbing the Purchase Items

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>

Hooking up the @submit

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.

Form Prevent Default

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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// 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
    }
}
userVoice