Back to blog
Blog

How to Build a Vue.js Photo Gallery

Ivan Larionov's avatar

Ivan Larionov

August 03, 2017

cover image

In this tutorial I’m going to show you how to build a photo gallery with a ready-to-use template from HTML5UP, powered by the Cosmic API, hosted on the Cosmic App Server.

TL;DR

View demo
View the codebase on GitHub

Prerequisites

You’ll need Node JS and npm. Make sure you already have them before you start.

Getting Started

First of all we’ll need to install VueJS CLI and start the new project.  Run the following commands to do this:

npm install -g vue-cli
vue init webpack vuejs-photo-gallery
cd vuejs-photo-gallery
npm install

After you’ll setup this project you’ll be able to run

cd vuejs-photo-gallery
npm run dev

And play with your app in browser

Doing everything using the existing git repo

First of all, you have to be sure you have node > 6.x installed, than run the following commands:

npm install -g vue-cli
git clone https://github.com/cosmicjs/vuejs-photo-gallery.git
cd vuejs-photo-gallery
npm install
npm run dev

Browser window will open automatically once you'll run the last command

Setting up Cosmic library

First of all, install Cosmic Angular/JavaScript library

npm install cosmicjs --save

Now you should be able to import Cosmic object and perform Cosmic API calls like following:

import Cosmic from 'cosmicjs';
const bucket = { slug: 'your-bucket-slug' };

Cosmic.getObjects({ bucket }, (err, res) => {
  console.log(res.objects);
});

Setting up things with Cosmic

Create the bucket and remeber the bucket name (vuejs-photo-gallery in our case):

Than create a new object type named Photo.

We also need a way to store the picture itself. Please enter the “Metafields Template” tab and add “Image/File” type metafield with key image. This metafield will store the image. We don’t need anything more, so just set the name and save object type. After save you’ll be redirected to ‘New Photo’ page. Create some photos using this page and save them - we'll use them as test data.

The only thing left is to set site-wide things, such as title, tagline, social icons and footer text. Let's create one more object type named Global. And add the following metafields:

  • Tagline - Plain Text Area
  • Twitter - Plain Text Input
  • Instagram - Plain Text Input
  • Github - Plain Text Input
  • Email - Plain Text Input
  • Footer - Plain Text Area

VueJS environments

We want to pick our bucket name automatically on deploy. In this case we'll need configuration file, which we'll populate with correct data during deploy. Create src/config.js to match the following:

Config = {
    bucket: 'vuejs-photo-gallery'
};

module.exports = Config;

Prepare assets

Download the template ZIP and unzip it somewhere. In our case we have the following content:

index.html - this is our HTML markup, we'll move it to Vue components later. images - this is sample images folder. We don't need it, our images will be served from Cosmic servers assets - other assets such as CSS, fonts, javascript files. We'll need CSS and fonts. Let's ignore Javascript for now, since we're planning to use VueJS. Let's copy assets/css and assets/fonts folders to static folder inside our project. This will allow us to add these files to the build automatically as static assets.

Prepare index.html

Now it's time to include our assets to index.html. Add the following to the head section:

<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--[if lte IE 8]><script src="assets/js/ie/html5shiv.js"></script><![endif]-->
<link rel="stylesheet" href="static/css/main.css" />
<!--[if lte IE 8]><link rel="stylesheet" href="assets/css/ie8.css" /><![endif]-->
<!--[if lte IE 9]><link rel="stylesheet" href="assets/css/ie9.css" /><![endif]-->
<noscript><link rel="stylesheet" href="static/css/noscript.css" /></noscript>

This will include our static assets. Once we'll implement correct markup, appearance will be automatically set via CSS.

VueJS components

Looking into our page, we can define components we'll need:

Header and Footer - for this will be very simple components, which will display global data which will be loaded on app startup Thumbs - this component will display photos thumbnails Viwer - this component will display the big photo and provide prev/next navigation. Please note - we're creating component's templates markup using template we downloaded before (doing copy-paste from index.html and applying VueJS directives).

Header component:

<template>
    <header id="header">
        <h1>{{ header }}</h1>
        <div v-html="text"></div>
        <ul class="icons">
            <li><a :href="twitter" class="icon fa-twitter"><span class="label">Twitter</span></a></li>
            <li><a :href="instagram" class="icon fa-instagram"><span class="label">Instagram</span></a></li>
            <li><a :href="github" class="icon fa-github"><span class="label">Github</span></a></li>
            <li><a :href="email" class="icon fa-envelope-o"><span class="label">Email</span></a></li>
        </ul>
    </header>
</template>

<script>
import {EventBus} from '../event_bus';

export default {
    name: 'app-header',
    created() {
        EventBus.$on('global_loaded', (obj) => {
            this.header = obj.title;
            this.text = obj.metafield.tagline.value;
            this.twitter = obj.metafield.twitter.value;
            this.instagram = obj.metafield.instagram.value;
            this.github = obj.metafield.github.value;
            this.email = 'mailto:' + obj.metafield.email.value;
        });
    },
    data () {
        return {
            text: null,
            twitter: '',
            instagram: '',
            github: '',
            email: '',
            header: ''
        }
    }
}
</script>

Footer component is very similar:

<template>
    <footer id="footer">
        <div v-html="text"></div>
    </footer>
</template>

<script>
import {EventBus} from '../event_bus';

export default {
    name: 'app-footer',
    created() {
        EventBus.$on('global_loaded', (obj) => {
            this.text = obj.metafield.footer.value;
        });
    },
    data () {
        return {
            text: null
        }
    }
}
</script>

Both components uses EventBus to receive data from parent component. I'll tell about the Event bus later in this post.

Thumbs component

This component is more complicated than previous two:

<template>
    <section id="thumbnails">
        <article v-for="(item, index) in items" v-bind:class="{ 'active': activeIndex == index }">
            <a class="thumbnail" v-on:click="selectImage(item, index)">
                <img v-bind:src="item.metafield.image.imgix_url" alt="" />
            </a>
            <h2>{{ item.title }}</h2>
            <div v-html="item.content"></div>
        </article>
    </section>
</template>

<script>
import Cosmic from 'cosmicjs';
import * as Config from '../config';
import {EventBus} from '../event_bus';

const bucket = { slug: Config.bucket };

export default {
    name: 'thumbs',
    props: ['bus'],
    created() {
        Cosmic.getObjectType({ bucket }, { type_slug: 'photos' }, (err, res) => {
            this.items = res.objects.all;
            EventBus.$emit('loaded', this.items[0]);
        });
        EventBus.$on('move', (dir) => {
            this.activeIndex = this.activeIndex + dir;
            if (dir > 0 && this.activeIndex >= this.items.length) {
                this.activeIndex = 0;
            }
            if (dir < 0 && this.activeIndex < 0) {
                this.activeIndex = this.items.length - 1;
            }
            EventBus.$emit('loaded', this.items[this.activeIndex]);
        });
    },
    data () {
        return {
            items: [],
            activeIndex: 0
        }
    },
    methods: {
        selectImage (itm, index) {
            EventBus.$emit('loaded', itm);
            this.activeIndex = index;
        }
    }
}
</script>

On component creation we're fetching our photos list and subscribes to Event bus 'move' event. Than we just render these photos and emit a new event each time when user selects a new photo.

Viewer component

<template>
    <div id="viewer">
        <div class="inner">
            <div class="nav-next" v-on:click="selectNext()"></div>
            <div class="nav-previous" v-on:click="selectPrev()"></div>
        </div>
        <div class="slide active" v-if="img">
            <div class="caption">
                <h2>{{ img.title }}</h2>
                <div v-html="img.content"></div>
            </div>
            <div class="image" v-bind:style='{ backgroundImage: "url(" + img.metafield.image.imgix_url + ")" }'>
            </div>
        </div> 
    </div>
</template>

<script>
import {EventBus} from '../event_bus';

export default {
    name: 'viewer',
    props: ['bus'],
    created() {
        EventBus.$on('loaded', (obj) => {
            this.img = obj;
        });
    },
    data () {
        return {
            img: null
        }
    },
    methods: {
        selectNext() {
            EventBus.$emit('move', 1);
        },
        selectPrev() {
            EventBus.$emit('move', -1);
        }
    }
}
</script>

This component subscribes to loaded event via Event bus. This event means that uses selected a new photo and we have to show it bigger size; Another task of this component is to notify Thumbs component when users click prev/next buttons. Component uses Event bus for this purpose also.

Event bus

Event bus follows publish-subscribe pattern and allows us setup communication between Thumbs and Viewer components. Both components are on the same level (no parent-child relationship), so we need something more complicated than simple event emission. Event bus implementation is very easy (src/event_bus.js):

import Vue from 'vue';
export const EventBus = new Vue();

This event bus is used to fire events in one component (using EventBus.$emit) and subscribe on them in another component (using EventBus.$on).

Concatenating everything together

Now it's time to concat everything with App component:

<template>
  <div>
    <div id="main">
      <app-header></app-header>
      <thumbs></thumbs>
      <app-footer></app-footer>
    </div>
    <viewer></viewer>
  </div>
</template>

<script>
import AppHeader from './components/AppHeader'
import AppFooter from './components/AppFooter'
import Thumbs from './components/Thumbs'
import Viewer from './components/Viewer'
import Vue from 'vue';
import Cosmic from 'cosmicjs';
import * as Config from './config';
import {EventBus} from './event_bus';

const bucket = { slug: Config.bucket };

export default {
  name: 'app',
  components: {
    AppFooter,
    AppHeader,
    Thumbs,
    Viewer
  },
  created() {
    Cosmic.getObjectType({ bucket }, { type_slug: 'globals' }, (err, res) => {
      EventBus.$emit('global_loaded', res.objects.all[0]);
      console.log(res.objects.all[0]);
    });
  },
}
</script>

This component loads globals data on creation and notify AppHeader and AppFooter components via EventBus.

Deploy to Cosmic servers

Cosmic has some requirements for deploying apps:

In our case we actually have HTML5 app, so we'll need some additional software.

Prepare config

Create a prepare.js file in your project directory:

var fs = require('fs');

var str = `
    Config = {
        bucket: '${process.env.COSMIC_BUCKET}'
    };

    module.exports = Config;
`;
fs.writeFile("./src/config.js", str, function(err) {
    if(err) {
        return console.log(err);
    }
    console.log("The file was saved!");
});

This script will rewrite application config file (see more info above) file to use your Cosmic bucket write key and bucket name.

Modify package.json

VueJS CLI adds some packaged on package.json as devDependencies. We have to move them all into dependencies to make our scripts work in Cosmic servers.

Prepare software

We'll also need something to serve our Angular app. We'll use Express framework:

npm install --save express

Add the following to your package.json:

{
  ...
  "scripts": {
    ...
    "start": "node app.js"
  },
  ...
}

The main point is to have start command defined in the scripts section (you can safely replace default angular start command). This is the command which will be run to start our app. So now we have the only thing left - create the app.js file:

const express = require('express')
const app = express()

app.use(express.static('./dist'));

app.listen(process.env.PORT, function () {
});

This is a simple Express app which serves dist dir as dir of static files. Please take note - app listens on port specified via PORT environment variable, it's important to run apps on Cosmic App Server.

Build VueJS app for production

We'll use app.json to do this (dokku predeploy section):

{
    "scripts": {
        "dokku": {
            "predeploy": "node prepare.js && npm run build"
        }
    }
}

This script will be executed before we'll launch our express app to build the VueJS app for production.

Run it!

Now you can enter 'Deploy Web App' page in your Cosmic Dashboard.

Simply enter your repo URL and click 'Deploy to Web' - deploy process will be started and app become ready in a couple of minutes.

Conclusion

Using Cosmic App Server allows quickly deploy the application to hosting using a git repo and don't worry about server configuration and software installation - everything will be done by Cosmic servers.