Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

AJAX & Delayed Rendering

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.

Let's start building the product page. In the component, we have the productId, but not the product data. We'll need an AJAX call to get that.

AJAX call for Product Data

In services/products-services.js, this is where we've been centralizing the AJAX logic for products. Let's add a new method to fetch just one: export function fetchOneProduct() with an iri argument.

... lines 1 - 34
export function fetchOneProduct(iri) {
... line 36
}

My favorite part about these IRI strings is that they are also URLs. So we can say return axios.get() and literally pass it iri.

... lines 1 - 34
export function fetchOneProduct(iri) {
return axios.get(iri);
}

Pretty awesome. We can even add documentation to be extra cool: the argument will be a string and this will return a Promise... that will resolve to an AxiosResponse, which you can keep for some extra auto-complete if you want. And a description and... nice!

... lines 1 - 28
/**
* Gets a product from the API according to the IRI.
*
* @param {string} iri
* @return {Promise}
*/
export function fetchOneProduct(iri) {
return axios.get(iri);
}

Over in product-show.vue, we're definitely going to need to store the response from the AJAX call as a piece of data. Start there: add a data() function and return an object with a product key initialized to null. Oh, but... ESLint wants me to move data after props: that's just a coding standards thing.

... lines 1 - 6
<script>
... lines 8 - 9
export default {
... lines 11 - 17
data() {
return {
product: null,
... line 21
};
},
... lines 24 - 30
};
</script>

For the AJAX call, we want to make it as early as possible. Do that by adding a created() function. Let's think... we're also going to want to know when the AJAX call is still loading.... so add a loading data set to true.

... lines 1 - 6
<script>
... lines 8 - 9
export default {
... lines 11 - 17
data() {
return {
product: null,
loading: true,
};
},
... lines 24 - 30
};
</script>

Back in created(), wrap the AJAX call in a try block just to get some rudimentary error handling. Say this.product equals and use that new fetchOneProduct() function. Hit tab to auto-complete that so PhpStorm adds the import for us. Good job editor!

Pass the function this.productId. Oh, but what we really want to do is wait for this: add await and then, of course, the function now needs to be async. And since fetchOneProduct() resolve to an AxiosResponse, what we want - the product details - will live on its data key.

Cool! Finish by adding finally with this.loading = false.

... lines 1 - 6
<script>
import { fetchOneProduct } from '@/services/products-service';
export default {
... lines 11 - 17
data() {
return {
product: null,
loading: true,
};
},
async created() {
try {
this.product = (await fetchOneProduct(this.productId)).data;
} finally {
this.loading = false;
}
},
};
</script>

If I thought that this endpoint might fail for some legitimate reason, I'd add a catch, set an error on some data & display it. But this at least sets loading to false if something unexpected happens.

Using v-if for Truly Conditional Rendering

Ok! Up on the template, let's start rendering data! As a reminder, our API has a full page of documentation at http://localhost:8000/api. Fancy! Here, you can see what all of the different endpoints return. For example, this shows all the fields we expect to get back for a product, like name!

Back in the component, add an <h1> tag with {{ product.name }}.

<template>
<div>
<h1>{{ product.name }}</h1>
</div>
</template>
... lines 6 - 33

Easy enough! We make an AJAX call and set it on the product data. We also have a loading data, which we'll use in a second.

But when we check this in the browser...error! If we refresh... bah! It won't go away!

Cannot read property name of null

And it's coming from ProductShow. I'm pretty sure the problem is right here in the template and... it makes sense! When the template first renders, the product is null. Hence, error!

One way to avoid this is to initialize your product to an object with a name key... something like this. But I prefer a different solution... because this looks kinda hacky to me.

Instead, simply don't render the product until it's loaded. Doing this is easy. Up in the template, wrap the <h1> in another <div> because we will soon have other product stuff that we want to render conditionally. On that div, add v-if="product".

<template>
<div>
<div v-if="product">
<h1>{{ product.name }}</h1>
</div>
</div>
</template>
... lines 8 - 35

The v-if is important. If we used v-show, it would still try to execute the code inside... it would just be hidden. With v-if, the code is not executed at all.

So when we over over now and refresh... it's empty? And no error? Let me check the Dev tools: find ProductShow and... yea - the product data is still null.

That's because of a typo that you probably saw me make a minute ago. Down in created, it should be this.product equals.

Now... I don't even need to refresh: we instantly see the name. Well, nearly instantly: when we refresh, it waits until the product data is available, and then loads.

Loading Animation

Before we keep going, since the page is empty for a moment, let's adds a loading animation. Back in the component, import Loading from @/components/loading:

... lines 1 - 10
<script>
... line 12
import Loading from '@/components/loading';
... lines 14 - 39
</script>

add a components key - Loading:

... lines 1 - 10
<script>
... line 12
import Loading from '@/components/loading';
... line 14
export default {
... line 16
components: {
Loading,
},
... lines 20 - 38
};
</script>

then up in the template say <loading> with v-if="loading"!

<template>
<div>
<loading v-if="loading" />
... lines 4 - 7
</div>
</template>
... lines 10 - 41

Two things about this. One: we might not even need a loading data for this component because either the product is null - which means we're loading - or it's not... which means loading is done. Your call.

Two, notice I'm using v-if on the <loading> component. Until now, I've always used v-show for the loading component. Why the change? Well... it doesn't really matter. If a loading animation will be shown and hidden multiple times, I would definitely choose v-show because that's faster at hiding and showing. But since it will only load once... and then disappear forever, v-if allows the component to be completely destroyed. But really, these are micro optimizations.

Anyways, when we move over now and refresh... there we go! We see the loading animation and then the product name.

Hmm, except this h1 looks a bit different than our product list page. See: it has a smaller font over here. Why? Look at the catalog component: it renders a title component. Yep, we centralized our title into its own component in the last tutorial so that all titles would have the same "look". But in product-show, we are not using it yet!

Let's fix that next, which... will expose a problem in that component. It's too smart!

Leave a comment!

0
Login or Register to join the conversation
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