Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Current Product Id

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

So here's the plan: make our top-level Vue component - products.vue - able to render either the product list page or the product show page. Why are we making one component able to render two different pages? One reason is that this is basically how Vue router works: one top-level file leverages the Router to render one of many different components based on the URL. And, in this situation, I also want both pages to have the same sidebar. So we'll be re-using the layout from products.vue.

<template>
<div class="container-fluid">
<div class="row">
<aside :class="asideClass">
<sidebar
:collapsed="sidebarCollapsed"
:current-category-id="currentCategoryId"
:categories="categories"
@toggle-collapsed="toggleSidebarCollapsed"
/>
</aside>
<div :class="contentClass">
<catalog
:current-category-id="currentCategoryId"
:categories="categories"
/>
</div>
</div>
</div>
</template>
... lines 22 - 62

Passing a currentProductId Global Variable

So... how can this component determine what the current page is? We could parse the URL... or we could pass that info into Vue.

Look at the Twig template. When we're on a specific category page, for example, if I click "Office Supplies", we set a currentCategoryId global variable, which our Vue component reads to load that category. Now we're going to do the same thing again: add a currentProductId if we're on a product page.

In the controller... in the showCategory() action, the currentCategoryId is not the database id, it's the category IRI - like /api/category/5. We generate that by autowiring the IriConverterInterface service.

... lines 1 - 12
class ProductController extends AbstractController
{
... lines 15 - 27
public function showCategory(Category $category, IriConverterInterface $iriConverter, CategoryRepository $categoryRepository): Response
{
return $this->render('product/index.html.twig', [
'currentCategoryId' => $iriConverter->getIriFromItem($category),
'categories' => $categoryRepository->findAll(),
]);
}
... lines 35 - 42
}

Down in showProduct(), do the same thing: add IriConverterInterface $iriConverter and then pass a new variable: currentProductId set to $iriConverter->getIriFromItem() and pass it $product, which is the entity object that Symfony automatically queried for.

... lines 1 - 12
class ProductController extends AbstractController
{
... lines 15 - 38
public function showProduct(Product $product, CategoryRepository $categoryRepository, IriConverterInterface $iriConverter): Response
{
return $this->render('product/index.html.twig', [
'categories' => $categoryRepository->findAll(),
'currentProductId' => $iriConverter->getIriFromItem($product),
]);
}
}

By the way, we could pass the entire Product object into the template and then serialize it to JSON - similar to what we did with the categories. That would help us avoid an AJAX call for the product data and let Vue render a bit faster. I'm going to avoid that here... mostly to make our life a bit harder. Again, yay learning!

In the template, copy the currentCategoryId code... then change everything to currentProductId. So if that variable is defined, set a new global JavaScript variable and escape it for JavaScript, just in case.

... lines 1 - 12
{% block javascripts %}
... lines 14 - 15
<script>
... lines 17 - 22
{% if currentProductId is defined %}
window.currentProductId = '{{ currentProductId|e('js') }}';
{% else %}
window.currentProductId = null;
{% endif %}
... lines 28 - 29
</script>
... lines 31 - 32
{% endblock %}

Cool! So in theory, at the browser, if we click into a product, we should have that global variable. I'll try it in the console: window.currentProductId. Nice!

Reading the currentProductId in Vue

So how could we read this in a Vue component? Well... it's a global variable... so we could put window.currentProductId anywhere. But in the last tutorial, we started centralizing these global variables into a services/page-context.js file. As a reminder, services/ is a directory that - for the most part - holds files that make AJAX calls. But in the case of page-context, instead of reading data from AJAX calls, it reads global variables.

Add a new method here, export function getCurrentProductId() and make it return window.currentProductId. We can even impress of friends by adding some documentation.

... lines 1 - 9
/**
* Returns the current product id that's set by the server.
*
* @returns {string|null}
*/
export function getCurrentProductId() {
return window.currentProductId;
}

Beautiful!

To make sure this all works, let's render this value in our Vue component. To do that, we need to make it available in the template... because we can't just call random functions from up here. The easiest - and nicest - way to do that is via a computed prop. In the component, add a new one called currentProductId(). Inside, return getCurrentProductId() and hit tab to auto-complete that.

... lines 1 - 23
<script>
... lines 25 - 26
import { getCurrentCategoryId, getCurrentProductId } from '@/services/page-context';
... lines 28 - 29
export default {
... lines 31 - 42
computed: {
... lines 44 - 49
currentProductId() {
return getCurrentProductId();
},
},
... lines 54 - 63
};
</script>

When it auto-completed, PhpStorm automatically added the new import for me... though I don't love that it put this on multiple lines... I'll fix that.

Anyways, now that we have a currentProductId computed prop, we can use it up in the template right before the catalog component, which lists all the products: {{ currentProductId }}.

<template>
<div class="container-fluid">
<div class="row">
... lines 4 - 12
<div :class="contentClass">
Product: {{ currentProductId }}
<catalog
... lines 16 - 17
/>
</div>
</div>
</div>
</template>
... lines 23 - 66

I love it! Go check the browser. We don't even need to reload! It's already there.

I can feel the power! Now, how can we render something different in the component based on this value? There are a few options, including the tried and true v-if. But instead, we're going to use something fancier called a dynamic component. That's next.

Leave a comment!

2
Login or Register to join the conversation
Abelardo Avatar
Abelardo Avatar Abelardo | posted 2 years ago | edited

Hi there,
Under this text:
"In the template, copy the currentCategoryId code... then change everything to currentProductId. So if that variable is defined, set a new global JavaScript variable and escape it for JavaScript, just in case.",
you pasted the same controller section than above, not the template you mentioned in this text.

Reply

Hey @AbelardoLG!

You're right! Thanks for the report - it looks like we duplicated one code block two times (and the 2nd one is wrong). We'll get that fixed up ASAP!

Cheers!

Reply
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