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 SubscribeHead over to sidebar.vue
where we make the categories Ajax call. In Symfony, we often isolate complex logic - or logic that we need to reuse - into services. One of the most common places that we do that is for database queries: we almost always have a repository class that holds all the database queries for a specific table.
It's optional, but I'd like to do the same with my frontend code! I'd like to isolate all of my Ajax requests for a specific resource - like categories or products - into its own, reusable JavaScript module. Then, instead of having Ajax calls inside my components, all that logic will be centralized.
In Symfony, "Services" is kind of a generic word for any class that does work. But in this context, I'm using "services" to mean something slightly different. These "services" are API services... or, really, any code that loads data - whether that's via an Ajax call, local storage or reading global variables that we set in Twig.
Inside of js/services/
, create a new file called categories-service.js
. The -service
on the end is totally redundant since we're in a services/
directory, but I like to have descriptive filenames.
The services/
directory already holds one other file called page-context
. This has nothing to do with Ajax calls or APIs but it is something that returns data, which is why I put it here. Right now it reads a global variable, but if we decided later to load this via Ajax, it would still be a service.
In categories-service.js
export a function called, how about, fetchCategories()
. For the logic, copy the axios
line from sidebar
... and paste it here. PhpStorm helpfully imported axios
for me... but I'll tweak the quotes. Back in the function, return axios.get()
.
import axios from 'axios'; | |
... lines 2 - 5 | |
export function fetchCategories() { | |
return axios.get('/api/categories'); | |
} |
This is a really, really simple Ajax call, but at least we're centralizing the URL so that we don't have it all over the place. Let's also be good programmers and add some documentation above this: it returns a Promise
... which, actually, PhpStorm already knew without us saying anything.
But I won't add a description because the function name already describes this pretty well.
... lines 1 - 2 | |
/** | |
* @returns {Promise} | |
*/ | |
export function fetchCategories() { | |
... lines 7 - 9 |
sidebar.vue
Ok: in sidebar.vue
, let's use this! First, import the service: import {} from '@/services/categories-service'
. Inside the curly braces, grab fetchCategories
.
... lines 1 - 50 | |
<script> | |
... line 52 | |
import { fetchCategories } from '@/services/categories-service'; | |
... lines 54 - 85 | |
</script> | |
... lines 87 - 105 |
Now down in created
, life gets much simpler: const response =
- keep the await
because we still want to wait for the function to finish - then fetchCategories()
.
... lines 1 - 54 | |
export default { | |
... lines 56 - 79 | |
async created() { | |
const response = await fetchCategories(); | |
... lines 82 - 83 | |
}, | |
}; | |
... lines 86 - 105 |
I love this! And to clean up, since we're not using axios
directly in this component, we can remove the import.
The other place where we're making an Ajax call is in catalog.vue
to fetch the products. This one is a bit more complex because if we have a category, we need to pass a category
query parameter.
Since this Ajax call is for a different API resource, inside the services/
directory, create a third file called products-service.js
.
Start the same way: export function fetchProducts()
with a categoryIri
argument. I've been calling this categoryId
so far, but in reality, this is the IRI, so I'll give it the proper name here.
import axios from 'axios'; | |
... lines 2 - 6 | |
export function fetchProducts(categoryIri) { | |
... lines 8 - 15 | |
} |
For the logic, go back to catalog.vue
, copy the params
code... and paste it here. Let's also copy the response line, paste that here too and return axios.get()
.
Finally, for the params
, it's not this.currentCategoryId
but categoryIri
. So if (categoryIri)
then params.category = categoryIri
.
... lines 1 - 6 | |
export function fetchProducts(categoryIri) { | |
const params = {}; | |
if (categoryIri) { | |
params.category = categoryIri; | |
} | |
return axios.get('/api/products', { | |
params, | |
}); | |
} |
And... I need to fix my import code to use single quotes on axios
.
Before we use this, let's add some docs: the categoryIri
will be a string
or null
and this will return a Promise.
... lines 1 - 2 | |
/** | |
* @param {string|null} categoryIri | |
* @returns {Promise} | |
*/ | |
export function fetchProducts(categoryIri) { | |
... lines 8 - 17 |
That's looking great!
catalog.vue
Let's put it to use in catalog.vue
. Like before, start by importing it: import {} from
'@/services/products-service' and then bring in the fetchProducts
function.
... lines 1 - 21 | |
<script> | |
import { fetchProducts } from '@/services/products-service'; | |
... lines 24 - 62 | |
</script> |
Now, down in created()
, we don't need any of the params
stuff anymore. And the response
line can now just be response = await fetchProducts(this.currentCategoryId)
.
... lines 1 - 26 | |
export default { | |
... lines 28 - 45 | |
async created() { | |
this.loading = true; | |
let response; | |
try { | |
response = await fetchProducts(this.currentCategoryId); | |
this.loading = false; | |
} catch (e) { | |
... lines 55 - 57 | |
} | |
... lines 59 - 60 | |
}, | |
... lines 62 - 64 |
This is lovely: the created()
function reads like a story: set the loading
to true, call fetchProducts()
, then set loading
to false.
To finish the cleanup, remove the unused axios
import.
Phew! We just made a lot of changes so... let's make sure we didn't break anything. Do a full page refresh to be sure and... yea! Everything loads. The products and sidebar work on every page and our code is better organized.
Before we keep going, I want to mention a design decision that I made. In a service like products-service
, we are returning the Promise
from Axios. That means that when we use await
or chain a normal .then()
, what we will ultimately receive is the response... which we can then use to say things like response.data
.
But if you want, you could go a bit further in the service and add .then((response) => response.data)
.
By doing this, our function still returns a Promise. But instead of the payload of the promise being the full Ajax response, it will be the JSON data. To make the code work in catalog
, we would set the function directly to the data
variable.
That makes the function a bit nicer to use. But... I'm going to completely undo all of that. Why? Changing the function to directly give you the JSON data is nice. The problem is if you ever needed to read something from the response, like a header. If we only returned the data, reading a header wouldn't be possible.
That's why I typically keep my functions simple and return the Promise that resolves to the entire Response. I need to do more work in my component, but I also have more power.
Refresh one more time to make sure nothing broke! All good.
Next, hmm... I don't mind some of the loading on this page, but the categories in the sidebar are starting to get on my nerves! It makes the page look incomplete while it loads because the categories feel like part of the initial page structure. We can fix that by passing the categories from the server directly into Vue!
"Houston: no signs of life"
Start the conversation!
// package.json
{
"devDependencies": {
"@symfony/webpack-encore": "^0.30.0", // 0.30.2
"axios": "^0.19.2", // 0.19.2
"bootstrap": "^4.4.1", // 4.5.0
"core-js": "^3.0.0", // 3.6.5
"eslint": "^6.7.2", // 6.8.0
"eslint-config-airbnb-base": "^14.0.0", // 14.1.0
"eslint-plugin-import": "^2.19.1", // 2.20.2
"eslint-plugin-vue": "^6.0.1", // 6.2.2
"regenerator-runtime": "^0.13.2", // 0.13.5
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^8.0.0", // 8.0.2
"vue": "^2.6.11", // 2.6.11
"vue-loader": "^15.9.1", // 15.9.2
"vue-template-compiler": "^2.6.11", // 2.6.11
"webpack-notifier": "^1.6.0" // 1.8.0
}
}