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 SubscribeYay! Our customers can now add these top-quality products to their cart. Now we need a way for them to check out. Here's the plan: we're going to create a brand new page that holds a, sort of, "shopping cart" / "checkout" combo.
To create this page, if we want, we could build it entirely on the server-side with Twig. That's a great option if we don't need the power of Vue... which we definitely do not always need. But... you know, since this is a Vue tutorial, I was thinking that we should use Vue.
The first step to creating a page is... exactly the same whether it will use Vue or not: create a route, controller and template in Symfony. And... I've done that for us! Open src/Controller/CartController.php
:
... lines 1 - 9 | |
class CartController extends AbstractController | |
{ | |
/** | |
* @Route("/cart", name="app_cart") | |
*/ | |
public function shoppingCart(): Response | |
{ | |
return $this->render('cart/cart.html.twig'); | |
} | |
} |
This shoppingCart()
action will be called when we go to /cart
and it renders a template: templates/cart/cart.html.twig
:
{% extends 'base.html.twig' %} | |
{% block body %} | |
<h1>TODO - put a cart here!</h1> | |
{% endblock %} |
This holds a giant TODO!
The name of the route for this page is app_cart
. So, as a first step, let's link to this from the shopping cart total at the top of the page. Open templates/base.html.twig
. And... set the cart link href=""
to {{ path('app_cart') }}
:
<html lang="en-US"> | |
... lines 3 - 9 | |
<body> | |
<header class="header"> | |
<nav class="navbar navbar-expand-lg navbar-dark justify-content-between"> | |
... lines 13 - 22 | |
<ul class="navbar-nav"> | |
... lines 24 - 41 | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ path('app_cart') }}"> | |
Shopping Cart (<span id="js-shopping-cart-items">{{ count_cart_items() }}</span>) | |
</a> | |
</li> | |
</ul> | |
</nav> | |
</header> | |
... lines 50 - 69 | |
</body> | |
</html> |
Go back to the browser... refresh and click the shopping cart. Hello big, gigantic <h1>
tag!
Ok! Let's create the new Vue component for this page! But... wait... because we have two options about how to render that component.
First, we could make the products.vue
component even smarter and give it the ability to render one of three different components:
<template> | |
<div class="container-fluid"> | |
<div class="row"> | |
... lines 4 - 12 | |
<div :class="contentClass"> | |
<component | |
:is="currentComponent" | |
v-bind="currentProps" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 22 - 78 |
Remember: this dynamic component
is already capable of rendering the catalog
component or product-show
component based on which page we're on. We could, pretty easily, make this able to render a new shopping-cart
component if we're on the cart page.
A second option is to create a totally new Webpack entry that renders the new component. The benefit of this approach is that it splits the final JavaScript into two different files. If the user goes to the catalog or product page, they would download one JavaScript file containing the code for those two components, but not the code for the cart component. They would only download that code once they actually went to that page.
This is not the only way to split and optimize your code... and you should beware of premature optimizations. But this is something to keep in mind.
Anyways, for our new cart page, I'm going to create a totally separate Webpack entry file... in part so we can see how that approach looks.
In assets/pages/
- because that's where we're putting components that render the entire "main" part of the page - create a new file called shopping-cart.vue
. I'll paste in some content, which is pretty boring right now: a basic layout, a component that's empty and some CSS.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
<aside class="col-xs-12 col-lg-3" /> | |
<div class="col-xs-12 col-lg-9"> | |
<title-component text="Shopping Cart" /> | |
<div class="content p-3"> | |
TODO - show the cart! | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import TitleComponent from '@/components/title'; | |
export default { | |
name: 'ShoppingCart', | |
components: { | |
TitleComponent, | |
}, | |
}; | |
</script> | |
<style lang="scss" module> | |
@import '~styles/components/light-component.scss'; | |
.component :global { | |
.content { | |
@include light-component; | |
} | |
} | |
</style> |
Next, we need a new pure JavaScript "entry file" that will render this component. For the catalog and product show page, the entry file is assets/products.js
. On a high level, the purpose of an entry file is to execute all of the JavaScript needed for the page or pages where it's included. Since our entire page is being rendered in Vue... its only job is to render that component!
Copy products.js
and create our new shopping-cart.js
entry file. Inside, the only difference is that we want to render the shopping-cart
component.
import Vue from 'vue'; | |
import App from '@/pages/shopping-cart.vue'; | |
new Vue({ | |
render: (h) => h(App), | |
}).$mount('#app'); |
Finally, we need to tell Encore about the new entry file. Open webpack.config.js
, scroll down to addEntry()
, copy the one for products and change this to shopping-cart
and shopping-cart
.
... lines 1 - 9 | |
Encore | |
... lines 11 - 27 | |
.addEntry('products', './assets/products.js') | |
.addEntry('shopping-cart', './assets/shopping-cart.js') | |
... lines 30 - 97 | |
; | |
... lines 99 - 105 |
And because we just updated webpack.config.js
, this is a rare time when we need to restart Encore. Find your terminal, go to the tab that's running Encore, stop it with Control + C, and re-start it:
yarn dev-server
Perfecto! The result is that Encore is now outputting new shopping-cart
JS and CSS files that we can include on the page. Let's go do that!
Back to cart.html.twig
. This will look a lot like the template that renders the products
vue app: product/index.html.twig
. Copy the contents of this file, close it, and paste into cart.html.twig
.
{% extends 'base.html.twig' %} | |
{% block body %} | |
<div id="app"></div> | |
{% endblock %} | |
{% block stylesheets %} | |
{{ parent() }} | |
{{ encore_entry_link_tags('products') }} | |
{% endblock %} | |
{% block javascripts %} | |
{{ parent() }} | |
<script> | |
{% if currentCategoryId is defined %} | |
window.currentCategoryId = '{{ currentCategoryId|e('js') }}'; | |
{% else %} | |
window.currentCategoryId = null; | |
{% endif %} | |
{% if currentProductId is defined %} | |
window.currentProductId = '{{ currentProductId|e('js') }}'; | |
{% else %} | |
window.currentProductId = null; | |
{% endif %} | |
window.categories = {{ categories|jsonld }}; | |
</script> | |
{{ encore_entry_script_tags('products') }} | |
{% endblock %} |
Okay... let's see: this has a <div id="app">
- which is perfect because, in shopping-cart.js
, we're rendering into that element.
All we need to do is change the entry name to shopping-cart
in both the stylesheets
section and, at the bottom for the JavaScript file. And so far, we don't need any global variables... so I'll delete those. Remember: our new Vue component is, so far, almost completely empty.
{% extends 'base.html.twig' %} | |
{% block body %} | |
<div id="app"></div> | |
{% endblock %} | |
{% block stylesheets %} | |
{{ parent() }} | |
{{ encore_entry_link_tags('shopping-cart') }} | |
{% endblock %} | |
{% block javascripts %} | |
{{ parent() }} | |
{{ encore_entry_script_tags('shopping-cart') }} | |
{% endblock %} |
And... that's about as simple of a template as you can get! Let's try it! Find your browser, refresh and... tada! There's our Vue app! I'll even reopen my Vue dev tools... to see the component. Good start!
Next: one of the things that this component has in common with the product-show
component is that both need access to the shopping cart.... and both will need the ability to add items to the cart... because we're going to allow users to change the quantity of the items from this page. In fact, there are going to be a bunch of cart-related things that these two pages need to share. To tackle this, let's talk about mixins, which are Vue 2's way of sharing code between components. In Vue 3, mixins are replaced by the composition API. But both mixins and composition share the same fundamental goal and philosophy.
"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
}
}