Displaying beer details

Now we are going to have two different “pages” (or views) in our Vue Beers application, one for the beer list and another for a single beer detail.

Beer detail component

We are going to create a beer-detail component that calls to a beer details service (or in these case, a set of beer details JSON files) to recover and show more information on the chosen beer.

The template includes more beer properties, found in the detailed JSON file for each beer. We will use the Fetch API to get the file according to the beer id.


template: `
<div v-bind:id="beer.id" class="detail clearfix">
    <a href="#/">
        <img class="pull-right back" src="../../img/back.png">
    </a>
    <h1 class="name">{{beer.name}}</h1>
    
    <img class="pull-right img" v-bind:src="mainImg">
    
    <p class="description">{{beer.description}}</p>

    <ul class="beer-thumbs">
        <li>
            <img v-bind:src="imgUrl" 
                v-on:click="setImage(beer.img)">
        </li>
        <li>
            <img 
                v-bind:src="labelUrl" 
                v-on:click="setImage(beer.label)">
        </li>
    </ul>
    <ul class="specs">
        <li>
            <dl>
            <dt>Alcohol content</dt>
            <dd>{{beer.alcohol}}%</dd>
            </dl>
        </li>
    [...]
    </ul>
</div>
`;

Let’s add some CSS to make that prettier:

.detail {
margin: 10px;
padding: 10px;
border: solid 1px black;
border-radius: 10px;
min-height: 150px;
}
.detail .back {
width: 50px;
height: 50px;
}
.detail .img {
float: left;
border: 1px solid black;
margin-right: 3em;
margin-bottom: 2em;
background-color: white;
padding: 2em;
height: 400px;
width: 400px;
}
.detail .alcohol {
clear:both;
}

.detail ul.beer-thumbs {
margin: 0;
list-style: none;
}

.detail ul.beer-thumbs li {
border: 1px solid black;
display: inline-block;
margin: 1em;
background-color: white;
}

.detail ul.beer-thumbs img {
height: 100px;
width: 100px;
padding: 1em;
}

.detail ul.specs {
clear: both;
margin: 0;
padding: 0;
list-style: none;
}

.detail ul.specs > li{
display: inline-block;
width: 200px;
vertical-align: top;
}

.detail ul.specs > li > span{
font-weight: bold;
font-size: 1.2em;
}

.detail ul.specs dt {
font-weight: bold;
}

.detail h1 {
border-bottom: 1px solid gray;
}

We define a beer object in the data:

data: function() {
    return {  
        beer: {}, 
    };
},

And in order to get the details on the current beer as soon as possible, you can query the server from the mounted lifecycle hook:

mounted: function() {
    getBeerDetails(this.$route.params.id);
}

Reacting to Params Changes

One thing to note when using routes with params is that when the user navigates from /beer/AffligemBlond to /beer/Rochefort8, the same component instance will be reused. Since both routes render the same component, this is more efficient than destroying the old instance and then creating a new one. However, this also means that the lifecycle hooks of the component will not be called.

To react to params changes in the same component, you need to watch the $route object and query again the server to get the details:

  watch: {
    '$route' (to, from) {
        getBeerDetails(to.params.id);
    }
  }

Gettin the beer details

To get the details of a beer whose id is xxx we need to recover the file /vue-beers/data/beers/details/xxx.json. In the getBeerDetails method we are going to use again the Fetch API to recover the data, and initialise the beer data object with the details:

   methods: {
        getBeerDetails: async function(id) {
            let fetchResult
            fetchResult = await fetch(`../../data/beers/details/${id}.json`);
            if (fetchResult.status == 200) {
                this.beer = await fetchResult.json();
            }
        },
    },

Computed properties

In the details we want to display images for both the bottle and the label of the beer. In the JSON data we have a relative path for these images in beer.img and beer.label, but we need to adapt them to our application. The simplest way to do it is using two computed properties, imgUrl and labelUrl:

    computed: {
        imgUrl: function() {
            if (!this.beer.img) {
                return;
            }
            return `../../data/${this.beer.img}`;
        },
        labelUrl: function() {
            if (!this.beer.label) {
                return;
            }
            return `../../data/${this.beer.label}`;
        },
    },

Displaying the big picture

The two former images are thumbnails, and we would like to show a big version of them when they are clicked. Let’s begin by adding a big picture to our template:


<h1 class="name">{{beer.name}}</h1>

<img class="pull-right img" v-bind:src="mainImg">

<p class="description">{{beer.description}}</p>

We declare mainImg as a data member, with an empty string as initial value. We want the image to show the bottle image by default, so we set mainImg jut after recovering the beer details:

    data: function() {
        return {  
            beer: {}, 
            mainImg: null,
        };
    },
    [...]
    methods: {
        getBeerDetails: async function(id) {
            let fetchResult
            fetchResult = await fetch(`../../data/beers/details/${id}.json`);
            if (fetchResult.status == 200) {
                this.beer = await fetchResult.json();
            }
            this.mainImg = `../../data/${this.beer.img}`;
        },
    },

Then we need to listen for the click event on the two thumbnail images. In Vue we can do it with the v-on directive:

<ul class="beer-thumbs">
    <li>
        <img v-bind:src="imgUrl" 
            v-on:click="setImage(beer.img)">
    </li>
    <li>
        <img 
            v-bind:src="labelUrl" 
            v-on:click="setImage(beer.label)">
    </li>
</ul>
    methods: {
        setImage: function(img) { 
            this.mainImg = `../../data/${img}`;
        },
    },

And now you have your shiny new app with all the details!