Creating a masonry layout from scratch in VueJs (and CSS Grid)
Tags: webdev, vue, css, cssgrid
Published: 2019-07-25 11:32:01

---
title: Creating a masonry layout from scratch in VueJs (and CSS Grid)
published: true
description: I explain how I created a masonry layout for my blog site with vuejs and CSS Grid.
tags: #WebDev #VueJS #CSS #CSSGrid
---

I wrote my own website in Laravel and VueJS.  It’s nothing special, just a page about me and some of my skills, and a blog page.  Eventually I intend to have a page for some projects to showcase some of the things I've created in my spare time.

A few days ago, the blog index page was, well… shite, just a paginated list of the posts, nothing exciting and a pretty crap layout.  I wanted to spruce it up with a masonry feel, but didn’t want to import any packages, so had a go at building it myself.

This is resulting outcome. Check it out here [johnhalsey.co.uk/blog](https://johnhalsey.co.uk/blog)

![](https://thepracticaldev.s3.amazonaws.com/i/50g5rr2l0o25m6vpmprg.png)

So how did I achieve this?

Firstly, I make an api call to get the posts from the database.  I send back a JSON response in a Laravel resource (not shown here).


```javascript
export default {
    name: 'Posts',
    props: {
        category: {
            type: String
        }
    },
    data () {
        return {
            posts: []
        }
    },
    mounted () {
       this.getPosts()
    },
    methods: {
        getPosts () {
            let params = {}
            if(this.category != '') {
                params.category = this.category
            }
            window.axios.get('/api/posts', {params})
                .then(response => {
                    this.posts = response.data.data
                    this.calculateImageCount()
                })
        }
    }
}
```

The masonry layout is achieved with CSS Grid and a javascript calculation for each post that works out how many grid rows it needs to take up, but it can't start to work that out, until all the images have loaded and rendered on the browser, and since not all posts have images, I calculate how many of the posts have images.  


```javascript
data () {
    return {   
        imageCounter: 0
    }
},
methods: {
    calculateImageCount () {
        for (let i = 0; i < this.posts.length; i++) {
            if (this.posts[i].media.featured.medium != '') {
                this.imageCounter++
            }
        }
    }
}
```


Then in the template, when looping through the posts, I mark when each image is loaded, and increment a counter.



```html
<template>
    <div class="masonry">
        <div v-for="(post, key) in posts"
             :key="key"
             class="card"
        >
            <div class="card-content">
                <div v-if="post.media.featured.medium != ''">
                    <img :src="post.media.featured.medium" :alt="post.title" class="img-responsive" @load="rendered">
                </div>
                <div class="p-20">
                    <a :href="post.link">
                        <h3>{{ post.title }}</h3>
                    </a>
                    <a v-for="category in post.categories" :key="category.id" :href="'/blog?category=' + category.slug">{{ category.title }} <br></a>
                    <p class="font-12">Posted on {{ post.published_at }}</p>
                </div>
            </div>
        </div>
    </div>
</template>
```
```javascript
export default {
    data () {
        return {
            imagesCount: 0
        }
    },
    methods: {
        rendered () {
            this.imagesCount++
        }
    }
}
```


I watch the image counter and when it gets to the same number of images I know I’m expecting, I can calculate the height of the post cards.

```javascript
watch: {
    imagesCount: function () {
        if(this.imagesCount == this.imageCounter){
            this.resizeAllMasonryItems()
        }
    }
},
```

Then the magic happens.  the `resizeAllMasonryItems()` method loops through all the posts now that all images have loaded, and calls another method to actually resize the item, and it does that by applying a `grid-row-end: span [dynamic number]` style to each post card.

```javascript
resizeAllMasonryItems () {
    // Get all item class objects in one list
    let allItems = document.getElementsByClassName('card');

    /*
     * Loop through the above list and execute the spanning function to
     * each list-item (i.e. each masonry item)
     */
    for (let i = 0; i < allItems.length; i++) {
        this.resizeMasonryItem(allItems[i]);
    }
}
```

Each item gets passed into the `resizeMasonryItem()` method.


```javascript
resizeMasonryItem (item) {
    /* Get the grid object, its row-gap, and the size of its implicit rows */
    let grid = document.getElementsByClassName('masonry')[0],
        rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap')),
        rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));

    /*
     * Spanning for any brick = S
     * Grid's row-gap = G
     * Size of grid's implicitly create row-track = R
     * Height of item content = H
     * Net height of the item = H1 = H + G
     * Net height of the implicit row-track = T = G + R
     * S = H1 / T
     */

    let rowSpan = Math.ceil((item.querySelector('.card-content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));

    /* Set the spanning as calculated above (S) */
    item.style.gridRowEnd = 'span ' + rowSpan;
},
```

I style the parent element with some basic CSS, using CSS Grid.

```css
<style type="text/css">
    .masonry {
        display: grid;
        grid-gap: 15px;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        grid-auto-rows: 0;
    }
</style>
```

Finally, when the component is created I put event listeners on load and resize of the page to keep calculating the card heights.  Technically I don’t even need the load event listener, but seems nice to have it there.

```javascript
created () {
    let masonryEvents = ['load', 'resize'];
    let vm = this
    masonryEvents.forEach(function (event) {
        window.addEventListener(event, vm.resizeAllMasonryItems);
    });
}
```

That’s it, now I have a pretty cool masonry layout for my blog posts.  The whole thing looks like this.  And it's responsive too.

```vuejs
<template>
    <div class="masonry">
        <div v-for="(post, key) in posts"
             :key="key"
             class="card"
        >
            <div class="card-content">
                <div v-if="post.media.featured.medium != ''">
                    <img :src="post.media.featured.medium" :alt="post.title" class="img-responsive" @load="rendered">
                </div>
                <div class="p-20">
                    <a :href="post.link">
                        <h3>{{ post.title }}</h3>
                    </a>
                    <a v-for="category in post.categories" :key="category.id" :href="'/blog?category=' + category.slug">{{ category.title }} <br></a>
                    <p class="font-12">Posted on {{ post.published_at }}</p>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'Posts',
        props: {
            category: {
                type: String
            }
        },
        data () {
            return {
                posts: [],
                imageCounter: 0,
                imagesCount: 0
            }
        },
        mounted () {
            this.getPosts()
        },
        created () {
            let masonryEvents = ['load', 'resize'];
            let vm = this
            masonryEvents.forEach(function (event) {
                window.addEventListener(event, vm.resizeAllMasonryItems);
            });
        },
        watch: {
            imagesCount: function () {
                if(this.imagesCount == this.imageCounter){
                    this.resizeAllMasonryItems()
                }
            }
        },
        methods: {
            rendered () {
                this.imagesCount++
            },
            getPosts () {
                let params = {}
                if(this.category != '') {
                    params.category = this.category
                }
                window.axios.get('/api/posts', {params})
                    .then(response => {
                        this.posts = response.data.data
                        this.calculateImageCount()
                    })
            },
            calculateImageCount () {
                for (let i = 0; i < this.posts.length; i++) {
                    if (this.posts[i].media.featured.medium != '') {
                        this.imageCounter++
                    }
                }
            },
            resizeMasonryItem (item) {
                /* Get the grid object, its row-gap, and the size of its implicit rows */
                let grid = document.getElementsByClassName('masonry')[0],
                    rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap')),
                    rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));

                /*
                 * Spanning for any brick = S
                 * Grid's row-gap = G
                 * Size of grid's implicitly create row-track = R
                 * Height of item content = H
                 * Net height of the item = H1 = H + G
                 * Net height of the implicit row-track = T = G + R
                 * S = H1 / T
                 */

                let rowSpan = Math.ceil((item.querySelector('.card-content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));

                /* Set the spanning as calculated above (S) */
                item.style.gridRowEnd = 'span ' + rowSpan;
            },
            resizeAllMasonryItems () {
                // Get all item class objects in one list
                let allItems = document.getElementsByClassName('card');

                /*
                 * Loop through the above list and execute the spanning function to
                 * each list-item (i.e. each masonry item)
                 */
                for (let i = 0; i < allItems.length; i++) {
                    this.resizeMasonryItem(allItems[i]);
                }
            }
        }
    }
</script>

<style lang="scss" type="text/css">
    .masonry {
        display: grid;
        grid-gap: 15px;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        grid-auto-rows: 0;
    }
</style>
```