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 SubscribeOh no! The snacks category is empty! That's a huge problem on its own! To make things worse, you can't even tell! It looks like it's loading forever... while I'm sitting here getting hungrier and hungrier.
The reason is that, in the product-list
component, we're showing the loading animation by checking if the products length is zero.
Using the length of an array to figure out if something is done loading doesn't really work if it's possible that the thing really can be empty in some cases! And that's totally the situation we have: sometimes a category has no products. And later when we add a search, sometimes no products will match.
So... the easy solution didn't work. What we need instead is a flag that specifically tracks whether or not the products Ajax call is finished.
loading
to CatalogWe know Catalog is the smart component that takes care of making the Ajax request. This is means that it is also aware of whether or not we are currently making an Ajax call for the products. To track this, let's add a new data: call it loading
and set it to false
by default.
... lines 1 - 18 | |
<script> | |
... lines 20 - 23 | |
export default { | |
... lines 25 - 35 | |
data() { | |
return { | |
... line 38 | |
loading: false, | |
... line 40 | |
}; | |
}, | |
... lines 43 - 54 | |
}; | |
</script> |
Now, very simply, in created()
, say this.loading = true
right before the Ajax call and, right after, this.loading = false
.
And just like that, we have a flag that we can use to render things based on the true loading status!
... lines 1 - 42 | |
async created() { | |
... lines 44 - 48 | |
this.loading = true; | |
const response = await axios.get('/api/products', { | |
params, | |
}); | |
this.loading = false; | |
... line 56 | |
}, | |
... lines 58 - 60 |
While we're here, we can also add some simple error handling just in case the Ajax call fails. To do that, wrap all of this in a try...catch
block. Then, inside catch
, set this.loading = false
.
... lines 1 - 42 | |
async created() { | |
... lines 44 - 50 | |
try { | |
... lines 52 - 57 | |
} catch (e) { | |
this.loading = false; | |
} | |
}, | |
... lines 62 - 64 |
If we really didn't trust our API... we could add a data called error
, change that in catch to a message and render it. But with this, we will at least fail somewhat gracefully, and avoid the loader from spinning forever.
As easy as that was, this could be a bit dangerous! The problem right now is that if any of these lines have an error - like if our response doesn't have a data
key on it - then the catch will be called and we will not show it. We could be hiding a bug in our code. I hate bugs! I think pizza is much better...
So instead, above the try, add let response
. This simply declares the variable outside of the try...catch
scope so that it's available in the entire created
function. Now, remove the const
from response
and then I'll return
from the catch. So if we hit the catch, just exit. Finally, move the this.products = response.data
code outside of the catch. Now if that line has a problem, it won't be silenced: we'll have to deal with it!
... lines 1 - 42 | |
async created() { | |
... lines 44 - 48 | |
this.loading = true; | |
let response; | |
try { | |
response = await axios.get('/api/products', { | |
params, | |
}); | |
this.loading = false; | |
} catch (e) { | |
this.loading = false; | |
return; | |
} | |
this.products = response.data['hydra:member']; | |
}, | |
... lines 66 - 68 |
Whether or not you should use the try...catch
just depends on your situation. I probably wouldn't do this because, if my API endpoint is failing, I have bigger problems: my site is broken! Giving the user a graceful error is nice, but maybe I'll save that for V2.
However, if you do have a valid situation where an Ajax request might fail - like if you're sending data to the server that might fail validation - then this is how you can catch that error and deal with it. We'll talk about sending data in the next tutorial.
loading
down to product-list
Okay, we now have the loading
data on our smart catalog component. Let's pass that into the product-list
component so that we can use it to hide or show the loading spinner. Split the product-list
onto multiple lines and then add :loading="loading"
.
<template> | |
<div> | |
... lines 3 - 10 | |
<product-list | |
:products="products" | |
:loading="loading" | |
/> | |
... lines 15 - 18 | |
</div> | |
</template> | |
... lines 21 - 71 |
And now that we're passing the loading
prop, in index.vue
, update the props
so we can receive it: add a new loading
prop with type: Boolean
and required: true
.
... lines 1 - 17 | |
<script> | |
... lines 19 - 21 | |
export default { | |
... lines 23 - 27 | |
props: { | |
loading: { | |
type: Boolean, | |
required: true, | |
}, | |
... lines 33 - 36 | |
}, | |
}; | |
</script> |
We can now simplify the template: we want to show the loading animation if loading
is true. And we also want to show these product cards down here, if we are !loading
. This second spot isn't super important, but it doesn't hurt to have it!
<template> | |
<div class="row"> | |
<div class="col-12"> | |
<div class="mt-4"> | |
<loading v-show="loading" /> | |
</div> | |
</div> | |
<product-card | |
... line 10 | |
v-show="!loading" | |
... lines 12 - 13 | |
/> | |
</div> | |
</template> | |
... lines 17 - 40 |
Time to check things out! Yep! You can already see that the snacks page no longer has the loading spinner. And my other pages work just fine.
Well... except it would be even better with a "no products found" message! And now, we can easily add that.
After the <loading />
component, add an h5
with a v-show
directive. This will hold that "no products found" message... which means that we want it to show if we are not loading
but products.length === 0
.
<template> | |
... lines 2 - 3 | |
<div class="mt-4"> | |
<loading v-show="loading" /> | |
<h5 | |
v-show="!loading && products.length === 0" | |
class="ml-4" | |
> | |
Whoopsie Daisy, no products found! | |
</h5> | |
</div> | |
... lines 14 - 22 | |
</template> | |
... lines 24 - 47 |
If that's our situation, print a helpful message. And... there it is! Our snacks page - except for the fact that there are no snacks - works great.
The products loading part is now works flawlessly. But there is one other spot that we're loading with Ajax that does not have any loading info: the categories sidebar!
We're actually going to fix this soon by making the categories load instantly. But since they are still loading via Ajax, let's add the loading component there as well. Open up sidebar.vue
: this is the component that makes the Ajax request for the categories and renders them in its template.
To do this right, should we add another loading
data like we just did in catalog? We totally could! And that's probably a great option. But... I'm going to cheat because I know that my app will never have zero categories. If that ever happened, it would probably mean I accidentally emptied my database. Yikes!
Instead, I am going to use the categories.length
to figure out if we're loading. But to be extra organized, let's do this via a computed property called loading
. Inside return this.categories.length === 0
.
... lines 1 - 48 | |
<script> | |
... lines 50 - 51 | |
export default { | |
... lines 53 - 68 | |
computed: { | |
loading() { | |
return this.categories.length === 0; | |
}, | |
}, | |
... lines 74 - 78 | |
}; | |
</script> | |
... lines 81 - 99 |
If there are no categories, then we are loading! The nice thing about using a computed property is that it will let us use a simple loading
variable in the template. And later, if we did want to change this to data
, that would be super easy.
Ok: to use this in the template, first import the loading component: import Loading from '@/components/loading'
. Then add the components
key with Loading
inside.
... lines 1 - 48 | |
<script> | |
... line 50 | |
import Loading from '@/components/loading'; | |
... line 52 | |
export default { | |
... line 54 | |
components: { | |
Loading, | |
}, | |
... lines 58 - 82 | |
}; | |
</script> | |
... lines 85 - 103 |
Finally, up in the template, right after the h5
, we'll say <loading
with v-show="loading"
.
<template> | |
<div :class="[$style.component, 'p-3', 'mb-5']"> | |
<div v-show="!collapsed"> | |
... lines 4 - 7 | |
<loading v-show="loading" /> | |
... lines 9 - 38 | |
</div> | |
... lines 40 - 47 | |
</div> | |
</template> | |
... lines 50 - 105 |
I love it!
And when we move over to the browser... I'm hoping to see the loading animation right before the categories load. That was super quick! But it was there. We have proper loading on both sides!
Next, I want to start organizing our Ajax calls: we currently make them from inside of sidebar.vue
and catalog.vue
. That's maybe ok, but I'd like to explore a better way to organize these.
Hi Daniel!
I'm not sure I understand your question! Throwing errors inside catch is not something we'd want to do in our application. In our particular case, some times axios throws an error when the ajax request cannot be completed property, so wrapping the calls inside a try...catch is good practice.
Could you may be give us a code sample to illustrate what you mean?
A workaround to avoid the "infinite" loading is to show the categories which have items. Those haven't items simply wouldn't appear.
For example, Snacks wouldn't appear at the list of the left.
Hi Abelardo! Indeed that is a different approach you can take!
By going the route we went, we are able to teach some interesting stuff!
// 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
}
}
Why not also throw an error like a good boy inside the catch? `throw new Error(e);`