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 SubscribeSetting a global variable in JavaScript and then reading it from inside our Vue app is, really, a nice way to communicate information from our server to our front-end app. But global variables are still global variables and we should try to at least isolate and organize them as much as possible. Because, for example, what if we changed our app to use the Vue router? Instead of full page refreshes where we set the currentCategoryId
as a global variable in Twig, now that data would be returned in a different way: via an Ajax call.
The point is: the way we get currentCategoryId
could change. And if we have window.currentCategoryId
sprinkled around our code everywhere, it's... not ideal. No problem! Let's isolate our our global variable into a central spot. Enter JavaScript services!
I think you're really going to like this. Inside my js/
directory, create a new folder called services/
. So far, everything we've worked on has been Vue components... but there's a lot more to our app. We have code for making Ajax calls - which we will eventually centralize - and we're also going to have generic logic that we want to reuse from multiple places.
In our app, the services/
directory is going to hold files that help us fetch data. So, it's a bit different than services in Symfony, which are any classes that do work. By services here, I mean API services... though you could also fetch data from local storage... or even by reading a global variable that we set in the template! Those are all sources of data.
So inside services/
, create a new file called page-context.js
. I totally invented that name: the purpose of this file will be to help read data related to what "page" we're on - like the current category id.
Inside, instead of exporting a default, like we've been doing so far with Vue, we're going to export named functions. As we start adding more functions to this file, you'll see how we can use whichever one or two functions we need. Say export function
, call it getCurrentCategoryId
and, inside, very simply, return window.currentCategoryId
!
/** | |
* Returns the current category id that's set by the server. | |
* | |
* @returns {string|null} | |
*/ | |
export function getCurrentCategoryId() { | |
return window.currentCategoryId; | |
} |
Just like that! We have a central place to read our global variable! I'll celebrate by adding some JSDoc above this.
Thanks to this, if we get this information some other way in the future, we will only need to update code in this one spot. I love that!
Now, in sidebar.vue
, we can use this like any normal JavaScript module. But I'm going to type this import a little backwards: import from '@/services/page-context'
. I left the part after import
blank, which is totally not valid JavaScript. But now, I can add {}
and autocomplete the getCurrentCategoryId
function.
... lines 1 - 48 | |
<script> | |
... line 50 | |
import { getCurrentCategoryId } from '@/services/page-context'; | |
... lines 52 - 76 | |
</script> | |
... lines 78 - 96 |
Nice! Down in the computed property, use this: return getCurrentCategoryId()
.
... lines 1 - 52 | |
export default { | |
... lines 54 - 65 | |
computed: { | |
currentCategoryId() { | |
return getCurrentCategoryId(); | |
}, | |
}, | |
... lines 71 - 75 | |
}; | |
... lines 77 - 96 |
That is lovely! When we move over and refresh... it works!
I'm really happy that we've centralized this global variable into a shiny new module. But I want to do just a little bit of future proofing. In a real app, you may or may not choose to do this - but it'll be a good exercise and it will help us later.
Here we go: currentCategoryId
is not something that will change while our app is running. Because, when we click on a different category, the page refreshes and the entire Vue app restarts. For the duration of our page view, currentCategoryId
never changes!
This means that it isn't something that needs to live in data
: we don't need anything to re-render when it changes. That's why it's totally legal to grab this value in sidebar.vue
or anywhere else that needs it.
But I want to kind of future proof our app... and plan ahead for a future where currentCategoryId
will change while my Vue app is running. If you pretend that currentCategoryId
did need to be in data
, what component would that data
live in?
Remember: the answer to this question is always find the deepest component that needs the data. If I look in products.vue
, we know that sidebar
needs to know the currentCategoryId
so that it can highlight that category. And catalog
is also going to need to know the current category soon so that we can print the category title and filter the product list to show only those in that category.
This means that if currentCategoryId
were data, it would need to live on the products
components so that we could passed it down into sidebar
and catalog
as a prop.
Now, I don't actually want to turn the currentCategoryId
into data
right now because... I don't need to. But I do want to structure my app with this in mind. To start, copy the computed property from sidebar.vue
, and, in products.vue
add it there.
... lines 1 - 17 | |
<script> | |
... lines 19 - 20 | |
import { getCurrentCategoryId } from '@/services/page-context'; | |
... line 22 | |
export default { | |
... lines 24 - 33 | |
computed: { | |
... lines 35 - 40 | |
currentCategoryId() { | |
return getCurrentCategoryId(); | |
}, | |
}, | |
... lines 45 - 49 | |
}; | |
</script> |
Oh, and this is cool! When I pasted, check it out! It added the import for me automagically! It did mess up the code style, but you can fix that in PhpStorm if you want. That's better.
So, instead of having currentCategoryId
as data, we will have it as a computed property... but inside the component where it would, in theory, need to live as data. That will make it super easy to change to data later if we need to.
Now, pass this to sidebar with :current-category-id="currentCategoryId"
.
<template> | |
... lines 2 - 4 | |
<sidebar | |
... line 6 | |
:current-category-id="currentCategoryId" | |
... line 8 | |
/> | |
... lines 10 - 16 | |
</template> | |
... lines 18 - 53 |
And in sidebar.vue
, instead of a computed property, we'll set this as a prop. Add currentCategoryId
with type String
- this is the IRI string - and also default: null
.
... lines 1 - 48 | |
<script> | |
... lines 50 - 51 | |
export default { | |
... line 53 | |
props: { | |
... lines 55 - 58 | |
currentCategoryId: { | |
type: String, | |
default: null, | |
}, | |
}, | |
... lines 64 - 73 | |
}; | |
</script> | |
... lines 76 - 94 |
The reason I'm using default null
is that this will allow the prop to be a String or null
, which is what it will be on the homepage. You can add more customized prop validation if you want... but this is good enough for me!
If you scroll down, our currentCategoryId
computed property is angry! It says duplicate key currentCategoryId
because we don't want to have this as a prop and also as a computed prop. Delete the computed property... and we can also delete the import to celebrate. Our code is happy!
Moment of truth! When we move over... yes! It's still working. Yay for not breaking things!
If you're not sure why we did this, here's what's going on. By moving currentCategoryId
up to products.vue
and passing it as a prop to sidebar
, it would now be very easy for us to change the currentCategoryId
computed prop into data
. In fact, if we did that, everything else would... well... magically not break!
Next, let's get to work on the catalog
component. Let's pass currentCategoryId
as a prop so we can filter the list of products to only those for that category.
Hi, Kiuega!
It's part of the magic in Vue! :D
Your property is called `currentCategoryId` but if you type `current-category-id` in your Vue template, Vue will automatically translate it for you so that you can refer to it in JavaScript!
Hi there,
When I refresh the main page, it looks a bit weird. I can temporarily see a page with no styles and it is annoying: could we avoid it?
Hey @AbelardoLG!
Excellent question! You're only seeing this because of this change: https://symfonycasts.com/sc...
We have "disabled CSS extraction" in dev mode only. This causes there to be NO real link tags on the page. Instead, the CSS is injected via your JavaScript. We do this in dev only (this will not happen on production builds) and we only do it so that HMR (hot module replacement) with the dev-server can work: it requires that you disable CSS extraction. So, it's not a problem on production.
Btw, with Encore 1.0 (and Webpack 5), I believe that you do NOT need to disableCssExtraction anymore to get HMR working with CSS. So if you upgrade, I think you can remove that line and still enjoy HMR.
Cheers!
// 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
}
}
Hello here ! There is something that I am not sure I fully understood.
We have in <b>products.vue</b>:
<b>'current-category-id'</b> which will be passed as props to <b>sidebar.vue</b>
In <b>sidebar.vue</b> :
'<b>currentCategoryId</b>' != '<b>current-category-id</b>'. How is it that it works anyway? If we had passed, from <b>products.vue</b>, '<b>currentCategoryId</b>' instead of '<b>current-category-id'</b>, the result would be the same?
Thanks !