Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Product Listing Components

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We're only printing the name of each product, but we could easily start adding more content right here: the product image, price and a button to the view the product. But if we did that, this template we start to get pretty big. And later, we're going to add a search bar... which will make this component even busier!

Exactly when you should split a component into smaller components is not a science. It's a subjective decision and there's no wrong answer. But I am going to refactor this <div class="row"> into a product-list component: a component whose entire job will be to just... list products! At the end of the next chapter, I'll give you another reason why I did this.

Creating a Component Sub-Directory

You might expect me to go into the components/ directory and create a new file called product-list.vue. But... I won't. That's not wrong, but instead, I'm going to create a new directory called product-list/. Because, in a few minutes, we're going to have two components that help us build this product-list area.

Now create a new file called index.vue. I'll talk about that name soon.

Start the same way as always: add a <template> tag and, inside, since we are going move this entire area here, use the <div class="row"> as the outer element.

<template>
<div class="row">
</div>
</template>
... lines 6 - 18

Next, add the <script> tag with export default the options object. Give this a name set to, how about, ProductList.

... lines 1 - 6
<script>
export default {
name: 'ProductList',
... lines 10 - 15
};
</script>

Move the products Data?

Poetry! I already know that we're definitely going to need access to the array of products so that we can loop over them. To get this, we have two options. First, we could move the products data from catalog into here. After all, I said that a piece of data should live in the deepest component that needs it. And when we're done, that would be this component!

But... I won't do that. I'll talk about why in the next chapter. Moving the products data into here wouldn't be wrong, but leaving it in catalog will help us follow a "smart component, dumb component" design pattern. More on that soon.

The Imprecise products Prop

The second option - and the one we'll choose - is to have the products passed to us as props. Add a props option with a products key. The type will be Array - because this will be an array of product objects - and I'll also add required: true.

... lines 1 - 7
export default {
... line 9
props: {
products: {
type: Array,
required: true,
},
},
};
... lines 17 - 18

This is one of the downsides of props... or kind of JavaScript in general: I can say that this should be an Array, but I can't really enforce that it's an array of product objects: an array of objects that all have certain keys. Well, you can do this with custom prop validator logic, but I think it's more work than it's worth. So, I'll just use Array and say "Good enough for JavaScript!"

In the template, since we already have the outer div, copy the div with the v-for and paste it here. In theory, we shouldn't need to change anything: we're going to pass that same array of product objects into this component.

<template>
<div class="row">
<div
v-for="product in products"
:key="product['@id']"
class="col-xs-12 col-6 mb-2 pb-2"
>
{{ product.name }}
</div>
</div>
</template>
... lines 12 - 24

Importing and index Component

So... let's use this! Back in catalog, start by importing the component: import ProductList - that seems like a pretty good name - from @/components/product-list.

... lines 1 - 18
<script>
... lines 20 - 21
import ProductList from '@/components/product-list';
... lines 23 - 41
</script>

And I can stop right there. Because we have an index.vue inside of product-list/, we can import the directory and Webpack will know to use that index.vue file. That's a nice trick for organizing bigger components: create a sub-directory for the component with an index.vue file and add other sub-components inside that directory. We'll do that in a minute.

Anyways, add ProductList to components so that we can reference it in the template. Here, our job is pretty simple: delete the entire <div> and say <product-list />. PhpStorm is already suggesting the one prop that we need to pass: products="products". But we know that's wrong: we don't want to pass the string products, we want to pass the variable.

It's kind of fun to see what it looks like if you forget the colon. In the console of our browser, if we refresh, yikes!

Invalid prop: type check failed for prop "products": expected Array got String with value "products".

Add the : before products to make that attribute dynamic. And back over on the browser... yea! All the products are printing just like before.

<template>
<div>
... lines 3 - 10
<product-list :products="products" />
... lines 12 - 15
</div>
</template>
... lines 18 - 43

Creating a product-card Component

And once again, we could stop now and start adding more product info right here. But I'm going to go one step further and refactor each individual product into its own component.

Inside the product-list/ directory, create a second component called, how about, product-card.vue. Start like we always do: with the <template>. Each product will use use this div with the col classes on it. Copy the classes but keep the v-for here. In product-card, add a div and paste those classes.

Let's also go steal the {{ product.name }} and put that here.

<template>
<div class="col-xs-12 col-6 mb-2 pb-2">
{{ product.name }}
</div>
</template>
... lines 6 - 18

That's a good enough start. Next add the <script> tag with export default and name set to ProductCard. And once again, we know that we're going to need a product passed to us. So I'll jump straight to saying props with product inside. The product will be an object... and like with the Array, there's not an easy way to enforce exactly what that object looks like. But we can at least say type: Object and also required: true in case I do something silly and forget to pass the prop entirely.

... lines 1 - 7
export default {
name: 'ProductCard',
props: {
product: {
type: Object,
required: true,
},
},
};
</script>

Ok! This is ready! Back in index.vue, we'll follow the same process to use the new component. This starts to feel a bit repetitive.. and I love that! It means we're getting comfortable.

Start with import ProductCard from and you can either say ./product-card, which is shorter, or @/components/product-list/product-card, which is more portable.

... lines 1 - 10
<script>
import ProductCard from '@/components/product-list/product-card';
... lines 13 - 25
</script>

To make this component available in the template, add the components key with ProductCard inside.

... lines 1 - 13
export default {
... line 15
components: {
ProductCard,
},
... lines 19 - 24
};
... lines 26 - 27

Finally, in the template, this is pretty cool: we now want to loop over the products and render the ProductCard itself each time. And putting a v-for on a custom component is totally legal. Replace the <div> with <product-card>, remove the class attribute and replace it with :product="product". And since this element no longer has any content, it can be self-closing.

<template>
<div class="row">
<product-card
v-for="product in products"
:key="product['@id']"
:product="product"
/>
</div>
</template>
... lines 10 - 27

That's really nice: we loop over products and render an individual ProductCard for each one. When we check the browser, it looks like it's working! I'll refresh just to be sure... I don't always trust hot module replacement. And... yep! It does work.

What I really like about the product-card component is that it's focused on rendering just one thing: a single product. Next, let's really bring the product to life by adding more data, styles and a computed property.

Leave a comment!

7
Login or Register to join the conversation
davidmintz Avatar
davidmintz Avatar davidmintz | posted 1 year ago | edited

I'm an old dude who is accustomed to old-fashioned, conventional PHP with old school Javascript (SCRIPT tags, no asset management tooling...). The question I've been wondering about is: say you have a backend admin page for updating products --/admin/products/update/123. In the old days, in a controller method, you would fetch the product entity and inject it into your template, then render a form already populated with the entity data -- the point being, render your form and entity in one step. With this stuff, it looks like you've got your cool Vue components and they are responsible for fetching data via xhr <i>after</i> the user loads the page that contains the components themselves. Thus, two http(s) requests instead of one. So my gut reaction is, that's inefficient. If your Vue app were contained in the same page as everything else, I guess you could actually inject your entity (in PHP) and output it manually, so to speak, as template variables under the Vue app's data key. But the Vue app lives elsewhere and besides, it's kind of a weird approach. So I am trying to fully wrap my mind around this different, modern approach. Why shouldn't I care about the additional request cycle to fetch the data?

I hope this makes sense. btw I am totally loving the tutorial.

1 Reply

Hello great team! question for both, Vue and LiipImagineBundle:
How to apply Liip's filter in vue.js file as we do in twig: " <img src="{{ asset('/relative/path/to/image.jpg') | imagine_filter('my_thumb') }}"/> " ?
Thanks in advance

Reply

Hey Alexander,

Good question! :) So, you can't write Twig code in JS files, but you can add a script tag in the Twig where set the final computed path after LiipImagine's filter applied, something like this:


    <script>
        window.MY_THUMB_URL = '{{ asset('/relative/path/to/image.jpg')|imagine_filter('my_thumb') }}';
    </script>

And then you can access that MY_THUMB_URL in your JS scripts on that page. Or, you can put that computed URL on a data attibute of any tag, and then read it from the JS as well.

I hope this helps!

Cheers!

1 Reply

Good approach. It seems shorter than the way i found : in my Vue file I fetch the path of cached file -- $resolvedPath = $imagineCacheManager->getBrowserPath('/my/path/image.jpg', 'filter_name'); in my controller. Thank you Victor

Reply

Hey Alexander,

Great, I'm happy to hear it helps! Yeah, this is probably the easiest way :)

Cheers!

Reply
Brandon Avatar
Brandon Avatar Brandon | posted 3 years ago

I was hoping you would address dynamic dependent drop downs in this tutorial but didn't see it. I feel like this tutorial shows everything one would need to do it, but I can't quite put it together. I've setup my script to console log TODO's each time a drop down is changed, but I'm struggling with putting the AJAX call in the methods. I load the first drop down just as this tutorial shows loading catalogs and it works well asynchronously in a created section. But I don't know how to pass the the selected options library ID to the next AJAX call so the next one populates. If you are planning on touching on this in future tutorials I'll look for it there, if not, any help would be appreciated. Thank you for all your work on these tutorials.

Reply

Hi Brandon!

I'm not entirely sure of what you are trying to accomplish, but if I assume correctly: When you have one Drop Down element that depends on weather or not another Drop Down element has been populated (and is selecting some item), you could just base the second Drop Down load upon an @onInput event on the first one. I'd just make sure that the second Drop Down data would be cleared if the first Drop Down data isn't loaded and take it from there. I hope this helps!

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": {
        "@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
    }
}
userVoice