Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Product Details & Smart vs Dumb 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

Let's really make these products come to life! Now that we have a component whose only job is to render a single product, this is going to be fun & clean.

The Product Card Template

I'll start by pasting some HTML into the template: you can copy this from the code block on this page. But there's nothing too interesting yet. We are referencing a few styles - $style['product-box'] and $style.image and we'll add a style tag soon for those. If you're wondering why I'm using the square bracket syntax, that's because JavaScript doesn't like dashes with the object property syntax: you can't say $style.product-box... which, yes, is annoying.

<template>
<div class="col-xs-12 col-6 mb-2 pb-2">
<div :class="$style['product-box']">
<div :class="$style.image">
<img
:alt="product.name"
:src="product.image"
class="d-block mb-2"
>
<h3 class="font-weight-bold mb-2 px-2">
{{ product.name }}
</h3>
</div>
<div class="p-2 my-3 d-md-flex justify-content-between">
<p class="p-0 d-inline">
<strong>${{ product.price }}</strong>
</p>
<button
class="btn btn-info btn-sm"
>
View Product
</button>
</div>
</div>
<hr>
<div class="px-2 pb-2">
<small>brought to you by {{ product.brand }}</small>
</div>
</div>
</template>
... lines 34 - 46

A little below this, we are using some product data. If you go back to the Vue dev tools and click on Catalog, each product has several fields on it, like brand, image - which is the URL to an image - name, price, and even stockQuantity.

So, for the image, we're using src="product.image" - with the : that makes this attribute dynamic - and we're rendering more data for the alt attribute, the product name and the product.price. We also have a button to view the product page... which isn't doing anything yet... then we print product.brand.

Hopefully this all feels pretty simple.

When you Forget the style Tag

If we move over now and check the console, ooooo:

Cannot read property product-box of undefined

Coming from product-card. Vue is telling us that the $style variable is undefined... which makes sense: we don't have a style tag yet! No problem: add the <style> with lang="scss". In fact, $style will be undefined until you have a style tag and that tag has the module attribute.

... lines 1 - 46
<style lang="scss" module>
... lines 48 - 65
</style>

For the styles itself, I'm going to import this scss/components/light-component.scss file, which is a Sass mixin. Add @import, then, to use the Webpack alias we created earlier, say ~ and the name of the alias. So ~styles/ to point to the scss directory - then components/light-component.

... lines 1 - 46
<style lang="scss" module>
@import '~styles/components/light-component';
... lines 49 - 65
</style>

Excellent! Now that we've done the hard work, I'll paste in a few more styles. This adds the .product-box and .image classes that correspond with the $style code in the template.

... lines 1 - 46
<style lang="scss" module>
... line 48
.product-box {
border: 1px solid $light-component-border;
box-shadow: 0 0 7px 4px #efefee;
border-radius: 5px;
}
.image {
img {
width: 100%;
height: auto;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
h3 {
font-size: 1.2rem;
}
}
</style>

Ok, I think we're ready! When we move over... hmm... I don't see any products. Let's refresh to be sure. And... yes! There they are! Each has an image, title, price and button.

Computed price Property

But, hmm: these prices aren't right. I would love to be able to sell blank CDs for $1,300... but that's not the real price. When you deal with prices, it's pretty common to store the prices in "cents", or whatever the lowest denomination of your currency is. The point is, this is 2,300 cents, so $23.

Yep, we have a formatting problem: we need to take this number, divide it by 100 and put the decimal place in the right spot. Yes, we have a situation where we need to render something that's based on a prop, but needs some processing first. Does that ring a bell? That's the perfect use-case for one of my absolute favorite features of Vue: computed properties!

Let's do this! Add a computed option with one computed property called price(). Inside, return this.product.price - to reference the price of the product prop and divide this by 100. Good start! To convert this into a string that always has two decimal points, we can use a fun JavaScript function that exists on any Number: .toLocaleString(). Pass this the locale - en-US or anything else - and then an options array with minimumFractionDigits: 2.

... lines 1 - 34
<script>
export default {
... lines 37 - 43
computed: {
... lines 45 - 48
price() {
return (this.product.price / 100)
.toLocaleString('en-US', { minimumFractionDigits: 2 });
},
},
};
</script>
... lines 56 - 77

Pretty cool, right? I'll even add some docs to our function. I'm over-achieving!

... lines 1 - 43
computed: {
/**
* Returns a formatted price for the product
* @returns {string}
*/
price() {
... lines 50 - 51
},
},
... lines 54 - 77

Now that we have a computed property called price, we can use it with {{ price }}, as if price were a prop or data.

<template>
... lines 2 - 15
<div class="p-2 my-3 d-md-flex justify-content-between">
<p class="p-0 d-inline">
<strong>${{ price }}</strong>
</p>
... lines 20 - 25
</div>
... lines 27 - 32
</template>
... lines 34 - 77

We know that computed properties - similar to props and data - are added as properties to the Vue instance, which is why we can say things like this.price. But behind the scenes, when we access this property, it will call our method. As a bonus, it even caches that property in case we refer to it multiple times.

Computed Properties with Arguments?

Oh, and by the way: this is one of the reasons why I created a specific component for rendering each product. If we did not have this component... and we were rendering this data inside the ProductList component, we wouldn't be able to use a computed property... because we would need to pass an argument: the product whose price we need to calculate. Instead, we would have needed to create a method... which isn't the end of the world, but is less efficient. Any time that you're creating a method to return data, it's a signal that you should considering refactoring into a smaller component that could use a computed property.

Anyways, now when we move over... we don't even need to refresh: there is our beautiful 30.00 price. What a bargain!

Smart and Dumb Components

Before we keep going, I want to circle back on a controversial decision I made earlier: the fact that we kept the products data inside catalog.vue even though the product-list component is technically the deepest component that needs it.

If you look at catalog.vue, it holds the AJAX call and pretty soon it will hold logic for a search bar. But... it doesn't render a lot of markup. I mean, yeah, it has an <h1> up here and a <div> down here, but its main job is to contain data and logic.

Compare this to the product-list component: index.vue. This doesn't have any logic! It receives props and renders.

Well... surprise! This separation was not an accident: it's a design pattern that's often followed in Vue and React. It's called smart versus dumb components, or container versus presentational components.

This pattern says that you should try to organize some components to be smart - components that make AJAX calls and change state - and other components to be dumb - that receive props, render HTML and maybe emit an event when the user does something.

product-card is another example of a dumb, or "presentational" component. Sure, it has a computed property to do some basic data manipulation, but this is just a component that receives a prop and renders, maybe with some minor data formatting.

To compare this to the Symfony world, one way to think about this is that a smart component is like a controller: it does all the work of getting the data ready. That might involve calling other services, but that's not important. Once it has all the data, it passes it into a template, which is like a dumb component. The template simply receives the data and renders it.

Like all design patterns, keep this in the back of your mind as a guide, but don't obsess over it. We're doing a good job of making this separation in some places, but we're not perfect either, and I think that's great. However, if you can generally follow this, you'll be happier with your components.

Next, now that we're loading data via AJAX, we need a way to tell the user that things are loading... not that our server is on fire and they're waiting for nothing. Let's create a Loading component that we can re-use anywhere.

Leave a comment!

20
Login or Register to join the conversation
davidmintz Avatar
davidmintz Avatar davidmintz | posted 2 months ago

Somebody at SymfonyCasts is a comedic genius! These products are hilarious. I want to buy the Velvis!

1 Reply

Haha, I'm glad you enjoy our silly (but well thought) jokes :)

1 Reply
davidmintz Avatar
davidmintz Avatar davidmintz | posted 2 months ago

Hey, I have gotten this far into the tutorial but must have done something that created a show-stopping problem that I don't know how to fix. When I start the server and try to load the index page, I get a fatal

PHP Fatal error: Uncaught ReflectionException: Class "Doctrine\Common\Cache\ArrayCache" does not exist in /opt/www/symfony/vue-symfony/vendor/symfony/dependency-injection/ContainerBuilder.php:1089

and similar whenever I try to do something like composer update or symfony console cache::clear

My research has come up with things like https://stackoverflow.com/questions/68652105/composer-install-update-trigger-class-doctrine-common-cache-arraycache-does-not but before I try their advice I thought I'd try asking here.

Any ideas? Thanks.

Reply

Hey David,

Hm, not sure, just a fast wild idea - could you try to clear the cache by removing the folder? i.e. execute "rm -rf var/cache" in your console. Does the problem still exist?

Cheers!

Reply
davidmintz Avatar

Yeah, I thought of that and tried it, but it didn't work -- same result. (Odd, because you might think if its complaint is that it can't clear the cache so you do it yourself, it might stop complaining, at least temporarily. But no.) So I went ahead and tried composer require doctrine/cache "^1.12" and that seems to work -- don't know if bad side effects will show up somewhere later on.

If I understand it correctly, this ArrayCache class is intended as nothing more than a sort of no-op, a placeholder for the dev environment, and doesn't actually cache anything in the sense of persisting from one request to the next. Kind of ironic to get stuck there. I know you guys have more than enough to do, but maybe if there's a future version of this tutorial, this would be something to take a look at.

Reply

Hey David Mintz

The doctrine/cache library was deprecated some time ago. I'm not sure how you ran into this problem but I'm guessing you upgraded the DoctrineBundle library. Try upgrading all doctrine libraries by running


composer upgrade "doctrine/*"


and clear the cache manually just in case rm -rf var/cache

Reply
davidmintz Avatar

Yes, I think I did mess around with composer and got myself into this. I will try this suggestion once I work up the nerve -- it will probably work, but I don't have a deep understanding of dependency management and hesitate to play around now that I have it working :-) Thanks!

1 Reply
Michael K. Avatar
Michael K. Avatar Michael K. | posted 1 year ago

Hi there
Thanks a lot for the awesome tutorials!!
One question, I have the same entity fields like you in the tutorial. In my database I see the column "image_filename" with "pen.png" etc.. But in the Vue inspector the product object has all fields like "name", but the "image_filename" is missing. Have you an idea whats going on?
Beste regards
Michael

Reply

Hey Michael! My first guess would be a problem with API platform itself! If clearing the symfony cache doesn't fix it, then may be you have made some changes to the Entities yourself and API platform is not exposing that particular field? Let me know if any of this makes sense!

Reply
Michael K. Avatar
Michael K. Avatar Michael K. | shadowc | posted 1 year ago

Hey Matias
I've copied 1:1 the entity code of the tutorial and cleared the cache. Also when I create a new field in the entities the API platform doesn't show this field. But it's only my testing app. Now I build my first app with the API platform and I hope there it works fine. ;-)

Reply

Hey Michael!

Let's see if we can get this figured out :).

> I've copied 1:1 the entity code of the tutorial and cleared the cache

That *is* strange.

> Also when I create a new field in the entities the API platform doesn't show this field

So the logic for whether or not a field shows up in API Platform is "fairly" simple (with quotes around it, because nothing in programming is always *that* simple). There are 2 big things to look for:

A) Does your field have a "getter" method? The naming is important here. If your property is called imageFilename, then you need a getImageFilename() method. If you actually called your *property* image_filename, then I think it needs to be getImage_filename(), but I don't recommend naming things like this.

B) Your property needs to have the @Group annotation above it with a group that matches the "groups" option of the normalizationContext on your entity (if you have one, which we do in our project).

Let me know if this helps! And, of course, check out our API Platform tutorials to learn a ton more ;).

Cheers!

Reply
Michael K. Avatar

Sometimes it's that easy. In your course code is the @Group annotation missing and i didn't saw it.
/**
* @ORM\Column(type="string", length=255)
*/
private $imageFilename;

And I found another little diffrent between tutorial and your course code:
:src="product.image" but in the entity the field has the name $imageFilename.

Thank you so much for your help!
Best regards!

Reply

Hey Michael!

Ah, I understand better! There's not an inconsistency with the course download and the video - they use the exact same code. But I forgot that I exposed the "image" field in a special way. Instead of exposing imageFilename, I used a custom ProductNormalizer class (you can see this if you download the course code), which added a custom "image" field, with the full URL to the image. I bet you're just missing this ProductNormalizer in your code, which is why you weren't seeing the field!

On an API Platform-level, both using Groups or a custom normalizer are valid way to expose a field (the Groups way is the more official way). I used a custom normalizer so that I could use a service to help generate the "image" value.

Cheers!

Reply
Michael K. Avatar

Hi Ryan
I got it! Thanks a lot for your really good explanations and help!

Cheers Michael

Reply
Brandon Avatar
Brandon Avatar Brandon | posted 2 years ago

At 57 seconds into this video you show all the different properties in the Dev Tools for a product. One of them that isn't talked about is colors. How would you go about accessing colors? Do you have to make another AJAX call or is it available?

Reply

Hi Brandon!

In the first episodes of Part 2 of this tutorial (To be released), we will be talking about the Product Page, which takes care of the colors!

If you want to jump ahead and play around with it, we basically fetch all colors in the database with a colors service (you can fiddle with API Platform to see which API call is needed for that) and then display the color choices with a simple color swatches selector.

Stay tuned!

Reply
Justin Avatar

FYI: The "Velvis" product image 404s on case-sensitive filesystems. This is caused by a "V" in AppFixtures.php on line 130 for the image name instead a "v" as it is in the filename.

Reply

Hey Justin!

Silly Mac! Letting me make mistakes like this! Thanks for letting me know - I totally see it! We can't have anyone missing the velvis photo ;)

I've just pushed up a fix to the course code (lowercase "v" in AppFixtures). Thanks for the report - I really appreciate it!

Cheers!

Reply
Todor Avatar

"In fact, $style will be undefined until you have a style tag and that tag has the module attribute."

It is still undefined even if you have the style tag and the module attribute but within style you don't have any styles.

Reply

That is correct, Todor! Thanks for pointing that out!

If the Style tag is empty, there will not be a $style object for us to use. As soon as we enter anything in our modular style block, $style will become available!

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
    }
}