gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
Our top level component - products.vue
- can now read the currentProductId
, which will either be set if we're on a product page or will be null if we're on the page that lists products, which is known as the "catalog" in our code.
<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"> | |
Product: {{ currentProductId }} | |
<catalog | |
:current-category-id="currentCategoryId" | |
:categories="categories" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 23 - 66 |
So here's the plan: I want both the product show and the catalog pages to use the same sidebar - this list of categories. But each will render something different for the central content part. The catalog will render the catalog
component and the product show page will render something different... like a new component that we'll create now.
Inside assets/components/
, create a new file called product-show.vue
. Inside, add the template - with just a div
and some text to start:
<template> | |
<div> | |
I'd ❤️ to see a Product here! | |
</div> | |
</template> | |
... lines 6 - 12 |
Next, add the script
tag with export default
an object with just name
set to ProductShow
:
<template> | |
... lines 2 - 4 | |
</template> | |
<script> | |
export default { | |
name: 'ProductShow', | |
}; | |
</script> |
Before we worry about rendering catalog
or product-show
, let's first see if we can render both. Start with import ProductShow from '@/components/product-show'
:
... lines 1 - 25 | |
<script> | |
... line 27 | |
import ProductShow from '@/components/product-show'; | |
... lines 29 - 68 | |
</script> |
Then, under components
, make that available with ProductShow
:
... lines 1 - 25 | |
<script> | |
... line 27 | |
import ProductShow from '@/components/product-show'; | |
... lines 29 - 32 | |
export default { | |
... line 34 | |
components: { | |
Catalog, | |
ProductShow, | |
Sidebar, | |
}, | |
... lines 40 - 67 | |
}; | |
</script> |
Finally, in the template, say <product-show/>
:
<template> | |
<div class="container-fluid"> | |
<div class="row"> | |
... lines 4 - 12 | |
<div :class="contentClass"> | |
Product: {{ currentProductId }} | |
<product-show /> | |
... lines 16 - 20 | |
</div> | |
</div> | |
</div> | |
</template> | |
... line 25 | |
<script> | |
... line 27 | |
import ProductShow from '@/components/product-show'; | |
... lines 29 - 32 | |
export default { | |
... line 34 | |
components: { | |
Catalog, | |
ProductShow, | |
Sidebar, | |
}, | |
... lines 40 - 67 | |
}; | |
</script> |
Easy enough! When we check the browser, it's already there. Awesome!
But of course, we don't want to render both components, we want to render either product-show
or catalog
based on the currentProductId
. How can we do that?
Well, one easy option is to use v-if
on each component, like v-if="currentProductId"
so that it only renders if currentProductId
is set. We would do the opposite for catalog
.
That is a fine option, really, a great option! But, since we already know how to use v-if
, I want to show you another way to do this. It's really the same behind the scenes, but this other way is even nicer if you need to toggle between more than two components. It's called a dynamic component. Oooo.
To use it, we first need to calculate which component should be rendered. Let's add a computed property to do this - call it currentComponent()
. Inside, use the ternary syntax: if currentProductId
does not equal null
, then we want to render ProductShow
. Yep, we're referencing the component variable that we imported. Else, we want to render Catalog
.
... lines 1 - 25 | |
<script> | |
... lines 27 - 32 | |
export default { | |
... lines 34 - 46 | |
computed: { | |
... lines 48 - 56 | |
currentComponent() { | |
return this.currentProductId !== null ? ProductShow : Catalog; | |
}, | |
}, | |
... lines 61 - 70 | |
}; | |
</script> |
How can we use this in the template to render whatever it returns? By leveraging a special component called... well actually it's called literally... <component>
! Clear out the product stuff and change catalog
to component
. This special tag can render any component. You tell it what to render via a special is
prop. Say :is="currentComponent"
.
<template> | |
<div class="container-fluid"> | |
<div class="row"> | |
... lines 4 - 12 | |
<div :class="contentClass"> | |
<component | |
:is="currentComponent" | |
:current-category-id="currentCategoryId" | |
:categories="categories" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 23 - 71 |
That's it! Test drive time! Back at the browser, let's click "All Products" and... yea! That works! It renders the catalog
component. Clicking on any category renders the same... but clicking on a product renders product-show
.
So far, this component... isn't very interesting and we're not even passing any props to it. But obviously, it will need to know the currentProductId
so that we can eventually make an AJAX call for the full product data. Let's add that prop: props
, call it productId
, and this will be type:
String because it's an IRI string. Also add required: true
.
... lines 1 - 6 | |
<script> | |
export default { | |
name: 'ProductShow', | |
props: { | |
productId: { | |
type: String, | |
required: true, | |
}, | |
}, | |
}; | |
</script> |
Over in products.vue
, hmm. It's a little weird: this component will either render the catalog
component or the product-show
... and those both need different props. These two props are for Catalog
. For now, I guess let's just pass all the possible props needed. Add :product-id="currentProductId"
.
<template> | |
<div class="container-fluid"> | |
<div class="row"> | |
... lines 4 - 12 | |
<div :class="contentClass"> | |
<component | |
... lines 15 - 17 | |
:product-id="currentProductId" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 24 - 72 |
Oh, and back in product-show.vue
, to see if this is working, change the text in the template to render the productId
prop.
<template> | |
<div> | |
I'd ❤️ to see Product {{ productId }} here! | |
</div> | |
</template> | |
... lines 6 - 18 |
Over at our browser... yea! It already works!
But... it is weird that - no matter which component we're rendering - we're always passing extra props: the first two are needed only for catalog
and the last only for product-show
. Is that ok? Ah... not really.
Other than being messy, by default, extra props show up as attributes. We can see this in the DOM: this div
has an extra category
attribute set to a bunch of weird [object]
things. I... don't love that.
No problem. Create another computed property. This time call it currentProps()
. Like currentComponent
, this will return only the props needed for the current situation. Use the ternary syntax again: if this.currentComponent
equals ProductShow
, we'll return one set of props, else, we'll return a different set. The first situation needs productId
set to this.currentProductId
. And then second needs... I think currentCategoryId
. Let me check... Yep! It needs currentCategoryId
and categories
. Oh, but while we use these kebab-case attributes in the template, we'll use camel case down in the computed prop: Vue takes care of normalizing all of that.
So: currentCategoryId
set to this.currentCategoryId
and categories
set to this.categories
.
... lines 1 - 22 | |
<script> | |
... lines 24 - 29 | |
export default { | |
... lines 31 - 43 | |
computed: { | |
... lines 45 - 56 | |
currentProps() { | |
return this.currentComponent === ProductShow ? { | |
productId: this.currentProductId, | |
} : { | |
currentCategoryId: this.currentCategoryId, | |
categories: this.categories, | |
}; | |
}, | |
}, | |
... lines 66 - 75 | |
}; | |
</script> |
Perfecto! But now... how do we use this? Usually we pass props to a component using v-bind
- like v-bind:categories
... or just :categories
, for short. That allows us to pass in one prop. But this time, we want to bind a bunch of props all at once. How can we do that?
The answer is with v-bind
set to an object. So v-bind=""
- but no :prop-name
- then currentProps
.
<template> | |
<div class="container-fluid"> | |
<div class="row"> | |
... lines 4 - 12 | |
<div :class="contentClass"> | |
<component | |
:is="currentComponent" | |
v-bind="currentProps" | |
/> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 22 - 78 |
Let's try it! I'll move over and refresh manually just to be safe. And... it works! No errors, no extra weird attributes and every page seems to be not exploding.
Ok: this is a good situation! We have two separate pages and a blank slate waiting for us to fill in the product details... once we get that data via AJAX. That's next.
"Houston: no signs of life"
Start the conversation!
// 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
}
}