In this tutorial we will be building a cross-platform, real-time Bookmarking Application using Electron, Firebase, and VueJs. We will be covering everything from setting up your project structure and configuring Webpack to creating the Electron app and building the Vue components.

Here is a screenshot of finished the Bookmarking App we will be building:

Bookmarking App Electron Firebase VueJs Screenshot

This tutorial is divided into 5 main sections:

  1. Setting Up the Project
  2. Configuring Webpack
  3. Creating the Electron App
  4. Building the Components in VueJs and Storage with Firebase
  5. Recapping

Feel free to follow along by cloning the finished code from the GitHub repo or working through each of the sections.

Setting up the Project

In this section we'll be setting up the directory structure for our project and installing the dependencies for our application. Here's what the overall folder structure is going to look like by the time we're done building this application:

Overall Folder Structure

We'll dig deeper into each directory and explain their specifics when we get to the appropriate section, but for now here's a high level overview of what each folder/file will contain:

  • app/ will contain the source code for our Vue components, filters, store, and Firebase logic that we write
  • dist/ will contain all the final code from app/, bundled using Webpack
  • static/ contains the CSS and JS used by our application
  • index.html is the page Electron will render when the app is started
  • main.js will start the Electron app and render index.html in the window
  • webpack.config.js will contain the necessary Webpack configurations (don't worry, we'll conquer this!)

Start off by creating a new folder and cd-ing into it:

mkdir bookmarking-app && cd bookmarking-app

Next, let's initialize a package.json to keep track of our dependencies:

npm init -y

We need to change the start script in our package.json, so that when we type npm start, our Electron application will be launched using main.js as the entry point. Modify the scripts section to look like this:

"scripts": {
    "start": "electron main.js"
}

Let's install the dependencies for our application:

npm install --save firebase vue jquery

and Electron as our dev dependency:

npm install --save-dev electron-prebuilt

Configuring Webpack

I know Webpack can be quite intimidating at first, but just hang in there and we'll slowly go through configuring and understanding it in this section.

We'll be using vue-loader, a loader for Webpack, which allows us to write the HTML, CSS, and JavaScript for a Vue component in a single .vue file, like so (image taken from the vue-loader docs):

vue-loader file format

This is great because it allows us to leverage the power of components in Vue by composing them to create larger applications. It also gives us a bunch of other neat features like being able to write in ES2015, hot-reloading, pre-processors, etc...

Let's go ahead and install all the dev dependencies that vue-loader needs. There are quite a few of them, however vue-loader requires these dependencies to transpile and bundle our VueJs components.

npm install\
  webpack webpack-dev-server\
  vue-loader vue-html-loader css-loader vue-style-loader vue-hot-reload-api\
  babel-loader babel-core babel-plugin-transform-runtime babel-preset-es2015\
  [email protected]5\
  --save-dev

Now that we have all the dependencies installed let's get our hands dirty and configure Webpack to use them. Create the webpack.config.js in the root of your project directory with the following skeleton:

var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: './app/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'build.js'
  },
  module: {
    loaders: [

    ]
  },
  babel: {

  },
  plugins: [

  ]
}

Let's go through each of the sections in the skeleton, explain what they're needed for, and populate them.

We start off by requiring the path and webpack modules, which shouldn't be of any surprise at this point.

Next we specify the entry property to be main.js in the app/ directory. This file will contain the root Vue instance which we will take a look at shortly. Since the root Vue component will be composed of all the other components, Webpack will take care of traversing and resolving these dependencies so you don't have to worry about them.

The output property tells Webpack that we want to bundle everything into a file called bundle.js and stick it in the dist/ folder.

module.loaders is an array of loaders which essentially tell Webpack what to do when it encounters a certain type of file. In this case we want to specify the 2 loaders that we installed:

  1. The vue-loader to bundle the HTML, CSS, and JS of our .vue files into a single JavaScript module
  2. The babel-loader to transpile our ES2015 JavaScript code to ES5

Let's go ahead and populate the module.loaders array with the settings for the vue-loader and the babel-loader:

module: {
    loaders: [
        {
            test: /\.vue$/,
            loader: 'vue'
        },
        {
            test: /\.js$/,
            loader: 'babel',
            exclude: /node_modules/
        }
    ]
}

For the vue-loader, we're telling Webpack to look for and process any file with the .vue extension, hence the regex in the test property. As for the babel-loader, we want Webpack to use the Babel transpiler on any .js files. However, note the exclude property which basically tells Webpack: don't bother processing any of the JavaScript files you find in the node_modules folder since it'll slow down the processing significantly.

Since we've specified the babel-loader in our Webpack config, we will have to provide the transforms we want Babel to apply. In our case we want to write in ES2015, so we specify the following Babel transforms:

babel: {
    "presets": ["es2015"],
    "plugins": ["transform-runtime"]
}

Lastly, we want to tell Webpack to include electron as an external dependency which can be done using the ExternalsPlugin:

plugins: [
    new webpack.ExternalsPlugin('commonjs', [
        'electron'
    ])
]

We now have our Webpack configuration file all set up. Once we've created the remaining files in the project, you can run the Webpack bundling process simply by typing:

webpack

If you'd like to learn more about Webpack and it's configurations, you can have a look at the List of Webpack Tutorials or check out the vue-loader docs for more Vue-specific configs.

I know we've spent quite a bit of time configuring and setting up our project. However, for most cases, you would be using boilerplates or CLI tools to get you up and running quicker. Nevertheless, I believe it's important to be able to thoroughly understand how everything fits together so that you can better customize your workflow.

Creating the Electron App

Now that we have all the configuration and setup out of the way, let's start building our Electron application. As mentioned earlier, main.js, in the root of our project directory, will act as the entry point for Electron.

This file is quite simple. It's sole purpose is to create an instance of Electron, create a browser window, and render the index.html file in that window.

Here's the entire main.js file:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

let mainWindow;

function createWindow () {
    // create the browser window
    mainWindow = new BrowserWindow({width: 800, height: 600});
    // render index.html which will contain our root Vue component
    mainWindow.loadURL('file://' + __dirname + '/index.html');

    // dereference the mainWindow object when the window is closed
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
}

// call the createWindow() method when Electron has finished initializing
app.on('ready', createWindow);

// when all windows are closed, quit the application on Windows/Linux
app.on('window-all-closed', function () {
    // only quit the application on OS X if the user hits cmd + q
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', function () {
    // re-create the mainWindow if the dock icon is clicked in OS X and no other
    // windows were open
    if (mainWindow === null) {
        createWindow();
    }
});

Since the bulk of our code will be in our VueJs components located in the app/ folder, the index.html file that Electron will render is quite simple as well:

<!DOCTYPE html>
<html>

<head>
  <title>Bookmarking App | coligo.io</title>
  <link rel="stylesheet" href="static/semantic.min.css">
  <link rel="stylesheet" href="static/style.css">
</head>

<body>
  <app></app>
  <script>window.$ = window.jQuery = require('jquery')</script>
  <script src="static/semantic.min.js"></script>
  <script src="dist/build.js"></script>
</body>

</html>

The <app></app> component is the root Vue component which will be composed of other smaller components to make up our Bookmarking App.

dist/build.js is the Webpack output that will contain all our Vue components and Firebase logic nicely bundled into a single JavaScript module.

static/style.css contains basic CSS for our Bookmarking App.

If you're wondering what Semantic is: it's a user interface library to help us build our Bookmarking App much quicker by re-using their beautiful UI components. You can think of it as an alternative to Bootstrap.

You can grab all the contents that go into the static/ folder from the GitHub repo. This will include all the CSS for the Vue components as well as the files required by semantic-ui.

Let's move on to the next section where we'll be building the individual components that make up our app and where all the action happens.

Building the Components with VueJs and Storage with Firebase

In this section we'll be building the individual Vue components and composing them with one another to create the Bookmarking App. We will also be creating our own simplified version of an object store to manage the state of our Vue application and handle all the communication with Firebase.

For larger applications it would make sense to use something along the lines of vuex. However, since our application is relatively simple, we will be following the store pattern outlined in the Vue docs which should be more than sufficient for our purposes.

Let's focus our attention on the app/ folder in this section. To give you a visual sense of how our Vue application will be structured, here's what the entire app/ directory will look like:

App Folder Structure

  • The components/ folder will contain the individual Vue components that will make up the Bookmarking App
  • The filters/ directory will house the 2 filters we will be creating: filtering by category and filtering by the user's search term
  • store/ will contain our simplified store and Firebase logic
  • App.vue is our root Vue component
  • main.js is the root Vue instance

Here is a graphical breakdown of what our components will look like:

Components Layout Bookmarking App VueJs

  • App - root component: red
  • Sidebar: blue
  • BookmarkList: green
  • Bookmark: orange

Creating the Store and Setting up Firebase

If you don't already have a Firebase account, you can set one up in under a minute - just go to the Firebase website and hit Sign Up With Google and you're done! You should now have an application already set up for you:

Firebase App URL

Grab the URL to your Firebase app because we'll be needing it for our store.

Before we start coding up the store, let's take a step back and understand what we want to achieve with it and what sort of data it will be maintaining. Our Bookmarking App will need 2 main sets of data:

  1. A collection of bookmarks that a user has created
  2. A collection of categories and their associated colors

These 2 collections will be represented as objects, both in our store and Firebase. To give you a sense of the structure of the data we'll be working with, I've exported the data stored in Firebase:

{
  "bookmarks" : {
    "-KE-NI-AQIM8L3ZC8_Ek" : {
      "category" : "Development",
      "title" : "Real-Time Analytics Dashboard",
      "url" : "http://coligo.io/real-time-analytics-with-nodejs-socketio-vuejs/"
    },
    "-KE-Od9opi-E7KvvG-LG" : {
      "category" : "Development",
      "title" : "Building Large-Scale Apps - VueJs",
      "url" : "http://vuejs.org/guide/application.html"
    },
    "-KE-OzR79eW51MP6B-B_" : {
      "category" : "Development",
      "title" : "Firebase Web Quickstart",
      "url" : "https://www.firebase.com/docs/web/quickstart.html"
    },
    "-KE-P94aT_jmOfUJWEJX" : {
      "category" : "Development",
      "title" : "Get started with Electron",
      "url" : "http://electron.atom.io/"
    }
  },
  "categories" : {
    "Development" : "blue",
    "Design" : "purple"
  }
}

We have a bookmarks object and a categories object. Each bookmark has the following properties: category, title, and url. The cryptic looking keys (eg: -KE-NI-AQIM8L3ZC8_Ek) are uniquely generated by Firebase to help identify the objects.

The categories object is made up of key-value pairs where the key is the category name and the value is the color that the user has associated to that category.

Now that we have an understanding of how our data will be represented, let's list out what we want to do with this data through the Bookmarking App:

  1. First and foremost, we want to maintain a local copy of the categories and bookmarks objects that will be in sync with the Firebase database at all times
  2. We want to be able to add and delete a category
  3. We want to be able to add and delete a bookmark

Go ahead and create the store/ directory with the index.js file inside it so that we can start implementing the requirements we listed above.

Let's start off by importing the modules we need: the EventEmitter to notify our components when the data has changed and Firebase to create references to our database, listen for events signaling a change in the data, and performing additions and deletions.

import { EventEmitter } from 'events'
import Firebase from 'firebase'

Next, we'll create the reference to our Firebase database as well as references to the categories and bookmarks objects. If you're not familiar with Firebase at all, you can have a look at their Quickstart Guide, however we wont be doing anything too advanced with it.

// ENTER YOUR FIREBASE URL BELOW
const db = new Firebase("https://YOUR_FIREBASE_APP.firebaseio.com/")
const categoriesRef = db.child('categories')
const bookmarksRef = db.child('bookmarks')
const store = new EventEmitter()

let categories = {}
let bookmarks = {}

db is a reference to the root of our Firebase database and from that we create 2 more child references to the categories and bookmarks objects in the database using the .child() method.

As mentioned earlier the store is an EventEmitter object which will send out events to notify any component interested in knowing when there has been an update in the categories and bookmarks objects.

The empty categories and bookmarks objects are going to be the local copies that we will serve up to the components using the EventEmitter. It's important to keep these 2 local copies up-to-date with what is stored in the database so that the components reflect the correct and up-to-date bookmark and category listings.

Thankfully, Firebase's event system makes it extremely easy to keep our local and remote (database) copies in sync. Using the db.on(event, callback) method, which any Firebase reference provides, we can be notified whenever a bookmark or category is updated/added/deleted in our database and update our local copies with the snapshot of that data (requirement #1):

db.on('value', (snapshot) => {
  var bookmarkData = snapshot.val()
  if (bookmarkData) {
    categories = bookmarkData.categories
    bookmarks = bookmarkData.bookmarks
    store.emit('data-updated', categories, bookmarks)
  }
})

Once we update the local copy with the new snapshot received, we emit a data-updated event to provide the components with the new categories and bookmarks.

Note: the method above is rather coarse-grained in dealing with updates. For instance, if a category is added, we are asking Firebase to send us all the bookmarks and all the categories stored in the database. This isn't the most efficient manner to deal with this, however for the purpose of this tutorial, it was chosen to simplify the update handling. Ideally you would separate the categories and bookmarks listeners and create individual handlers for child_added, child_removed, and child_changed instead of an all encompassing value listener.

Moving on to requirement #2: The ability to add and delete a category. Since we don't want each component to contain it's own category mutators which could potentially affect any other component as a side-effect, we will include these mutators in the store and provide a standardized interface which the components can use to add and delete catagories. This way all the state and mutators are contained in a single file: the store.

To add a category we will add a function to the store object which takes in a category object (eg: {"Development": "blue"}) and updates the categories reference and the update() method:

store.addCategory = (category) => {
  categoriesRef.update(category)
}

To avoid overly complicating the tutorial, when a user deletes a category, we will reassign any bookmarks that belong to that category to Uncategorized:

store.deleteCategory = (catName) => {
  // first check if an 'Uncategorized' category exists, if not, create it
  if (!('Uncategorized' in categories)) {
    categoriesRef.update({'Uncategorized': 'white'})
  }

  for (var key in bookmarks) {
    if (bookmarks[key].category === catName) {
      bookmarksRef.child(key).update({category: 'Uncategorized'})
    }
  }
  categoriesRef.child(catName).remove()
}

Finally, requriement #3: the ability to add and delete a bookmark. To add a bookmark we will pass in a bookmark object (eg: {"category": "Developement", "title": "coligo", "url": "http://coligo.io"}) and push it to the bookmarks object in the database. The push() function will automatically generate that unique key we saw at the beginning (eg: -KE-NI-AQIM8L3ZC8_Ek):

store.addBookmark = (bookmark) => {
  bookmarksRef.push(bookmark)
}

To delete a bookmark, we'll simply take that crypting looking key that Firebase assigned to our bookmarks object and call the remove() method on it:

store.deleteBookmark = (bookmarkId) => {
  bookmarksRef.child(bookmarkId).remove()
}

Don't forget to export the store object for our components to use:

export default store

Building the Root Vue Component

Now that we have the store in place, we can use it to create our root Vue component, App.vue. Create the App.vue file in the root of the app/ directory:

All our .vue files will be made up of 2 parts:

<template>
    <!-- The HTML template for our component -->
</template>

<script>
    // the Javascript for our component
    // We will export a Vue component options object here
</script>

We'll be keeping all our CSS in a single file in the static/ folder so we can focus on the important parts for now. However, if you did want to, you can include CSS for each component using the <style></style> tags in any .vue file as well.

As we saw in the graphical layout of our components, the root Vue component, App.vue is made up of 2 smaller components: the sidebar and the bookmark list. Since App.vue is the root of all our components, we want it to listen to any data-changed events from our store and provide the Sidebar and BookmarkList with the updated categories and bookmarks for them to render.

To do this, the App component will listen for any data-changed events coming from our store and update it's data property with the new categories and bookmarks listings. The categories and bookmarks on the data property will then be passed down as props to the Sidebar and BookmarkList components to be rendered. Once we have that flow of data set up, we'll let Vue's reactivity system take control of the rest!

This is what the template for our App.vue will look like:

<template>
    <div id="app">
    <sidebar
      :categories="categories"
      v-on:category-selected="setSelectedCategory">
      <!-- bind 'selected-category event to the event handler setSelectedCategory' -->
    </sidebar>
    <bookmark-list
      :bookmarks="bookmarks | filterByCategory selectedCategory"
      :categories="categories">
    </bookmark-list>
  </div>
</template>

You will notice 2 additional things I haven't mentioned earlier: v-on:category-selected and the filterByCategory filter. We will be covering these 2 in depth when we're building the sidebar. Essentially, they allow us to click a categoy within the Sidebar component, which triggers a category-selected event to it's parent, the root App component. This will then filter the list of bookmarks being displayed to only show the ones that belong to the selectedCategory using the filterByCategory filter.

As for the JavaScript for the App component:

<script>
  import store from './store'
  import Sidebar from './components/Sidebar.vue'
  import BookmarkList from './components/BookmarkList.vue'
  import { filterByCategory } from './filters'

  export default {

    components: {
      Sidebar,
      BookmarkList
    },

    data () {
      return {
        categories: {},
        bookmarks: {},
        selectedCategory: ''
      }
    },

    filters: {
      filterByCategory
    },

    created () {
      // assign the event handler `updateListings` to the `data-updated` event
      store.on('data-updated', this.updateListings)
    },

    methods: {
      // set the bookmarks and categories data properties to the new ones
      // received from the store
      updateListings (categories, bookmarks) {
        this.categories = categories
        this.bookmarks = bookmarks
      },

      setSelectedCategory (category) {
        this.selectedCategory = category;
      }

    }

  }
</script>

Since this will be the root component, we will include it in the main.js file in the root of app/ directory so that it can be rendered in the index.html file we pointed Electron to:

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: 'body',
  components: { App }
})

The Sidebar Component

Moving on to the Sidebar component, which will live in the components/Sidebar.vue file. It is perhaps the most complex of the components we will be creating with 5 primary roles:

  1. Render the list of categories that it receives as props from the root App component
  2. Allow a user to click on a category to filter the list of bookmarks by that category
  3. Allow a user to delete a category
  4. Trigger the BookmarkModal component to allow a user to add a new bookmark
  5. Trigger the CategoryModal component to allow a user to add a new category

Starting off with the overall template for the Sidebar component:

<template>
  <div>
    <div id="categories">
      <div id="cat-header">
        <h2><i class="bookmark icon"></i>Bookmark | coligo</h2>
      </div>
      <div class="container">
        <h2>Categories
          <span class="clickable right-float">
            <i @click="addCategory" class="add icon"></i>
          </span>
        </h2>
        <div class="ui list">
          <div class="item clickable">
            <div class="content">
              <a class="ui grey empty circular label"></a>
              <span @click="categorySelected('')">All</span>
            </div>
          </div>
          <div v-for="(name, color) in categories" class="item clickable">
            <div class="content">
              <a class="ui {{ color }} empty circular label"></a>
              <span @click="categorySelected(name)"
                :class="{selected: selectedCategory === name}">
                {{ name }}
              </span>
              <i v-if="name !== 'Uncategorized'" class="remove icon right-float"
                @click="deleteCategory(name)">
              </i>
            </div>
          </div>
        </div>
        <button @click="addBookmark"
          class="ui grey inverted basic icon circular button right-float">
          <i class="icon add"></i>
        </button>
      </div>
    </div>
    <category-modal></category-modal>
    <bookmark-modal :categories="categories"></bookmark-modal>
  </div>
</template>

There's nothing too special about the template for the sidebar component other than a couple of click event handlers and a v-for loop to render the categories. However it's worth noting a couple of things.

You will notice the All category that is hard-coded. This is done to allow the user to reset the filter and view all the the bookmarks that belong to any category.

All Category for VueJs Component

You will also notice v-if="name !== 'Uncategorized'" which shows a delete icon for all categories except the Uncategorized one. This is because when we delete a category, we move all it's bookmarks to Uncategorized. For the sake of keeping things simple in this tutorial we wont be dealing with editing categories and bookmarks, just adding and deleting.

Delete Category Icon

Let's have a look at the interesting part of the Sidebar component, the options object that it exports:

<script>
  import store from '../store'
  import CategoryModal from './CategoryModal.vue'
  import BookmarkModal from './BookmarkModal.vue'

  export default {

    data () {
      return {
        selectedCategory: ''
      }
    },

    props: ['categories'],

    components: {
      CategoryModal,
      BookmarkModal
    },

    methods: {

      addBookmark () {
        this.$broadcast('add-bookmark')
      },

      addCategory () {
        this.$broadcast('add-category')
      },

      deleteCategory (category) {
        store.deleteCategory(category)
      },

      categorySelected (category) {
        this.selectedCategory = category
        this.$dispatch('category-selected', category)
      }

    }

  }
</script>

We declare the CategoryModal and BookmarkModal as components since we used them within the template. The only props that the Sidebar component accepts is the categories object, which it renders using the v-for loop.

You may or may not be familiar with the $broadcast and $dispatch methods that Vue provides. So let's quickly go over what we're using them for.

When a user clicks the large plus button at the bottom of the sidebar to add a new bookmark, we want to trigger the bookmark modal to allow the user to input their new bookmark.

To do this we use the $broadcast method which allows the Sidebar component to trigger an event to all it's child components, particularly the BookmarkModal component which will take care of adding the bookmark for us.

Add Bookmark Button

The addCategory method works in the same exact way, but instead it triggers an add-category event for the CategoryModal to deal with:

Add Category Button

The deleteCategory(category) method simply works by invoking the delete method in the store that we created earlier.

Finally, the categorySelected(category) method uses the $dispatch method that Vue provides to inform the Sidebar component's parent, in this case the root App component that a new category has been selected. The App component will be listening to those events, as we saw earlier, and handle them using the setSelectedCategory(category) method. This will, in turn, cause the filterByCategory filter to update the bookmark list.

Category Select Dispatch Vue Event

(notice the Development category is selected and only bookmarks that are under the 'Development' category are displayed)

The BookmarkList Component

There's not much to the BookmarkList component other than rendering a collection of child Bookmark components and a simple filterByTitle filter which allows the user to type a search term and filter the list of rendered bookmarks accordingly. We will be looking at the implementation of the filterByTitle and filterByCategory at the end of this tutorial, so for now just assume they exist and we can import them into our components.

In the components/BookmarkList.vue file, we can define the template and functionality of the BookmarkList component, which is rather simple:

<template>
  <div id="links-container">
    <div id="toolbar">
      <div class="ui inverted icon fluid input">
        <input v-model="query" type="text" placeholder="Filter your links...">
        <i class="search icon"></i>
      </div>
    </div>
    <div class="ui relaxed divided selection list">
      <bookmark v-for="(id, bookmark) in bookmarks | filterByTitle query"
        :id="id"
        :title="bookmark.title"
        :url="bookmark.url"
        :category="bookmark.category"
        :category-color="categories[bookmark.category]">
      </bookmark>
    </div>
  </div>
</template>

<script>
  import Bookmark from './Bookmark.vue'
  import { filterByTitle } from '../filters'

  export default {

    data () {
      return {
        query: ''
      }
    },

    props: ['bookmarks', 'categories'],

    components: {
      Bookmark
    },

    filters: {
      filterByTitle
    }

  }

</script>

Since we will be using the Bookmark component as it's child component, we will define it in the components object. This leads us to the next step, building the Bookmark component!

The Bookmark Component

The Bookmark component (components/Bookmark.vue) takes in the id, title, url, category, and categoryColor for a single bookmark and renders it using the template defined. However, it also defines 2 interesting methods: deleteBookmark and openLink:

<template>
  <div @click="openLink" class="item">
    <div class="content">
      <i @click.stop="deleteBookmark" class="icon remove right-float"></i>
      <a class="header">{{title}}</a>
      <div class="description">
        {{url}}
        <a class="ui {{categoryColor}} tiny label right-float">{{category}}</a>
      </div>
    </div>
  </div>
</template>

<script>
  import { shell } from 'electron'
  import store from '../store'

  export default {
    props: ['id', 'title', 'url', 'category', 'categoryColor'],

    methods: {
      deleteBookmark () {
        store.deleteBookmark(this.id)
      },

      openLink () {
        shell.openExternal(this.url)
      }
    }

  }
</script>

The deleteBookmark() method simply invokes the deleteBookmark method on the store object we created earlier and passes it the ID of the bookmark to delete.

Delete Bookmark Component

openLink() is an interesting method. We are importing the shell module from electron so that we can use the openExternal(url) method to open the URL associated to this bookmark in the user's default browser.

Note: the URL must be a valid one which includes the protocol, eg: http://coligo.io

That's it for the Bookmark component! We can now move on to the last 2 components: the CategoryModal and the BookmarkModal.

The CategoryModal Component

The CategoryModal will take the user's input for a category name and category color then use the addCategory() method we defined on the store to add a new category. It will also listen for an add-category event coming from the Sidebar component to display the CategoryModal as we discussed earlier.

Category Modal Component

The components/CategoryModal.vue file will look like so:

<template>

  <div id="cat-modal" class="ui small modal">
    <i class="close icon"></i>
    <div class="header">
      Add a new category
    </div>
    <div class="content">

      <form class="ui form">
        <div class="field">
          <label>Category name</label>
          <input v-model="catName" type="text" placeholder="Enter a category name...">
        </div>
        <div class="field">
          <label>Category color</label>
          <select v-model="catColor" class="ui simple dropdown">
            <option value="">Select a color</option>
            <option v-for="color in categoryColors"
              value="{{color}}">
              {{color | capitalize}}
            </option>
          </select>
        </div>
      </form>

    </div>
    <div class="actions">
      <div @click="addCategory" class="ui purple inverted button">Save</div>
    </div>
  </div>

</template>

<script>
  import store from '../store'

  export default {

    data () {
      return {
        catName: '',
        catColor: '',
        categoryColors: ['red', 'orange', 'yellow', 'olive', 'green',
          'teal', 'blue', 'violet', 'purple', 'pink', 'brown', 'grey', 'black']
      }
    },

    methods: {

      addCategory () {
        var newCategory = {}
        newCategory[this.catName] = this.catColor
        store.addCategory(newCategory)
        $('#cat-modal').modal('hide')
      }

    },

    events: {

      'add-category': function () {
        this.catName = this.catColor = ''
        $('#cat-modal').modal('show')
      }

    }

  }
</script>

The Bookmark Modal

Since the BookmarkModal component behaves in a similar manner as the CategoryModal, their code will look quite similar. The BookmarkModal will accept the user's input for a bookmark title, url, and category. It will then construct a bookmark object and call the addBookmark(bookmark) method on the store to create the new bookmark in Firebase.

Unlike the CategoryModal, the BookmarkModal will also accept the categories object as a prop so that it can present the user with the available categories they may choose from when adding a new bookmark.

Bookmark Modal Component

The final components/BookmarkModal.vue, much like the CategoryModal, will look like so:

<template>

  <div id="bookmark-modal" class="ui small modal">
    <i class="close icon"></i>
    <div class="header">
      Add a new bookmark
    </div>
    <div class="content">

      <form class="ui form">
        <div class="field">
          <label>Bookmark Title</label>
          <input v-model="bookmarkTitle" type="text" placeholder="Enter a title for your bookmark...">
        </div>
        <div class="field">
          <label>Bookmark URL</label>
          <input v-model="bookmarkUrl" type="text" placeholder="Enter the URL for your bookmark...">
        </div>
        <div class="field">
          <label>Bookmark category</label>
          <select v-model="bookmarkCategory" class="ui simple dropdown">
            <option value="">Select a category</option>
            <template v-for="(name, color) in categories">
              <option value="{{ name }}">{{ name }}</option>
            </template>
          </select>
        </div>
      </form>

    </div>
    <div class="actions">
      <div @click="addBookmark" class="ui inverted purple button">Add</div>
    </div>
  </div>

</template>

<script>
  import store from '../store'

  export default {

    data () {
      return {
        bookmarkTitle: '',
        bookmarkUrl: '',
        bookmarkCategory: ''
      }
    },

    props: ['categories'],

    methods: {

      addBookmark () {
        const newBookmark = {
          title: this.bookmarkTitle,
          url: this.bookmarkUrl,
          category: this.bookmarkCategory
        }
        store.addBookmark(newBookmark)
        $('#bookmark-modal').modal('hide')
      }

    },

    events: {

      'add-bookmark': function () {
        this.bookmarkTitle = this.bookmarkUrl = this.bookmarkCategory = ''
        $('#bookmark-modal').modal('show')
      }

    }

  }
</script>

The Filters

This leads us to the last part of this tutorial. Actually creating the filters that we have been using in some of our components. You can have a look at the filters tutorial for a refresher on creating custom filters. We want to create 2 filters:

  1. filterByTitle to return a list of bookmarks that only contain that term in their title
  2. filterByCategory to return a list of bookmarks that belong to a certain category

In the root of the app/ directory create a directory for our filters and a filters/index.js file in which we will define those 2 filters:

export function filterByTitle (value, title) {
  return filterBookmarks(value, 'title', title)
}

export function filterByCategory (value, category) {
  if (!category) return value
  return filterBookmarks(value, 'category', category)
}

function filterBookmarks (bookmarks, filterBy, filterValue) {
  var filteredBookmarks = {}
  for (var bookmark in bookmarks) {
    if (bookmarks[bookmark][filterBy].indexOf(filterValue) > -1) {
      filteredBookmarks[bookmark] = bookmarks[bookmark]
    }
  }
  return filteredBookmarks
}

The filterBookmarks method takes the common code between filterByTitle and filterByCategory to modularize the code a little more.

Recapping

If you've made it this far in the tutorial, thank you! I know there was a lot of content to cover and 3 different technologies mixed in together, but hopefully you now have a basic understanding of how they can all fit together and you can put that knowledge into practice.

I just want to quick list off the things we covered in this tutorial and to hopefully refresh your memory with some of the key points:

  • We set up Webpack so that we can bundle all the Vue and Firebase code in the app/ folder into a single JavaScript module
  • We've set up a basic Electron application to render our Vue application
  • We've seen how to use .vue files with the help of vue-loader to build a modular, component-based user interface
  • We built a simple store as a means of maintaining the application's state in a single, centralized location
  • We were able to effortlessly keep the local bookmark and category listings in sync with the ones stored in the database thanks to Firebase
  • We used Vue's event system to $dispatch and $broadcast events between components

The Bookmarking Application we built was rather bare bones and there is a lot of room for improvement. Here are some ideas that you can try implementing yourself to put what you learned in practice and get your hands dirty:

  • Add editing functionality for bookmarks and categories
  • Allow users to merge 2 categories into a single one
  • Add authentication to your application

I'd highly encourage you to download the completed code from the GitHub repository and go through it one more time to have a complete picture of how everything fits together. If you have any questions, please post them in the comments section below or send me a tweet @coligo_io and I'll get back to you right away!