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 SubscribeLet's handle the really important case: when the POST request fails with validation errors. Look at the data from the response down in the console log. The data has this really nice violations
key... and then each violation has a propertyPath
that tells us exactly which field it should be attached to.
Love that! Inside of the checkout form component, we already built this validationErrors
object, which is... already being used by each field to figure out whether that field is invalid and what error message to show. So... basically, all we need to do is read violations
and map each one to the validationErrors
data. We planned ahead and it rocks.
Head down to onSubmit()
. The first thing we need to do is, at the beginning, add this.validationErrors =
an empty object so that any errors from a previous submit are cleared.
... lines 1 - 71 | |
<script> | |
... lines 73 - 76 | |
export default { | |
name: 'CheckoutForm', | |
... lines 79 - 103 | |
methods: { | |
... lines 105 - 118 | |
async onSubmit() { | |
... lines 120 - 121 | |
this.validationErrors = {}; | |
... lines 123 - 143 | |
}, | |
}, | |
}; | |
</script> |
This is actually going to cause a reactivity problem... a problem where things don't re-render at the right time. Don't worry about that yet: we'll talk about it deeply in a few minutes.
Down in else
, say response.data.violations
- that will be an array - .forEach()
and pass that a callback with a violation
argument. The logic we need inside is pretty simple: this.validationErrors[
violation.propertyPath
because each violation has that key... and, as you can see in the docs, it will be equal to the, sort of "id", we've been using for each field. Set this to violation.message
.
... lines 1 - 71 | |
<script> | |
... lines 73 - 76 | |
export default { | |
name: 'CheckoutForm', | |
... lines 79 - 103 | |
methods: { | |
... lines 105 - 118 | |
async onSubmit() { | |
... lines 120 - 123 | |
try { | |
... lines 125 - 130 | |
} catch (error) { | |
... lines 132 - 133 | |
if (response.status !== 400) { | |
this.serverError = true; | |
} else { | |
response.data.violations.forEach((violation) => { | |
this.validationErrors[violation.propertyPath] = violation.message; | |
}); | |
} | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
That... should do it! Move over, get to the checkout form and submit it empty. Woh! That's gorgeous!
Before we talk about client-side validation, I think we should finish onSubmit()
by handling the happy case: what happens when the form is valid and a new Purchase
was created in the API.
Open src/Controller/CheckoutController.php
. I've already created a really simple "order confirmation" page. All we need to do is get the id of the new Purchase
that was just created by the API and then redirect to /confirmation/{id}
that id.
... lines 1 - 10 | |
class CheckoutController extends AbstractController | |
{ | |
... lines 13 - 20 | |
/** | |
* @route("/confirmation/{id}", name="app_confirmation") | |
*/ | |
public function confirmation(Purchase $purchase): Response | |
{ | |
... lines 26 - 39 | |
} | |
} |
There's nothing really fancy about this. Back in Vue, inside of the successful side of things - the try
- add window.location =
and then we can use fancy ticks to say /confirmation/
, ${}
then response.data.id
.
... lines 1 - 71 | |
<script> | |
... lines 73 - 77 | |
export default { | |
name: 'CheckoutForm', | |
... lines 80 - 104 | |
methods: { | |
... lines 106 - 119 | |
async onSubmit() { | |
... lines 121 - 124 | |
try { | |
... lines 126 - 132 | |
window.location = `/confirmation/${response.data.id}`; | |
} catch (error) { | |
... lines 135 - 143 | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
This is not @id
, it's actually id
. If you look at the API docs, I don't always return an id
field on my resource... since every resource already has an @id
. But I did in this case because it made my life easier.
Oh, and there's one other function I'm going to call. In cart-services.js
, search for clearCart
. This makes an AJAX call that tells the API that the cart should now be deleted and reset. That could happen automatically on the server when we create a Purchase
, but since it doesn't in our API, we'll do it in Vue. Add await clearCart()
and hit tab so PhpStorm adds the import for us.
... lines 1 - 71 | |
<script> | |
... lines 73 - 75 | |
import { clearCart } from '@/services/cart-service'; | |
export default { | |
name: 'CheckoutForm', | |
... lines 80 - 104 | |
methods: { | |
... lines 106 - 119 | |
async onSubmit() { | |
... lines 121 - 124 | |
try { | |
... lines 126 - 130 | |
await clearCart(); | |
window.location = `/confirmation/${response.data.id}`; | |
} catch (error) { | |
... lines 135 - 143 | |
} finally { | |
this.loading = false; | |
} | |
}, | |
}, | |
}; | |
</script> |
So: clear the cart and then redirect to the confirmation page.
Let's try it! Go find the check out form, fill in some real data... this is - I kid you not - the real address of one of my friends - then some fake data and... submit!
It worked! The user now sees a very cutting-edge message about how they can mail us a check or money order to receive their products in a short three to six months. Oh, and also, the shopping cart on top shows zero: the cart was reset.
Before we go find our checkbook and a stamp, we're not quite done yet. Next, let's explore how we could also add client-side validation.
Hey Thomas!
Thank you for the head ups! And thanks for sharing your solution with others!
Cheers!
Hi victor (and Victor)!
I checked into this! In the course download, the @Groups({"purchase:read"})
above the id
property IS there. But... I did add it between part 1 and part 2 of this tutorial. So if you're coding along with your finished code from part 1, that would explain it :). I try to minimize changes between tutorials... but usually there are things I want to change to keep things really smooth (like this, I didn't want to talk about serialization groups in a Vue tutorial).
Anyways, thanks for commenting - it may help others.
Cheers!
// 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
}
}
Hi and thanks for the course!
/api/purchases
wasn't sendingid
, I added@Groups({"purchase:read"})
annotation insrc/Entity/Purchase.php
on$id
.Have a nice day!