How to use Vue.js with Electron and Vuex

Paul Halliday

Electron makes it easy to create cross platform desktop applications with JavaScript. As it uses the Node.js runtime, we have the luxury of using any frontend framework we want! In this article, we’ll be looking at getting up and running with Vue and Electron to create our own applications.

Whilst we could set up our environment ourself, it’s worthwhile to use a dedicated starter such as vue-electron.

Project setup

To get started, run the following in your terminal:

$ npm install vue-cli -g

$ vue init simulatedgreg/electron-vue my-todos

You’ll then be asked some questions about your project, fill them in to generate the config file:

Application Name > my-todos
Application Id > com.example.yourapp
Application Version > 0.0.1
Project Desription > Manage your Todos with Vue and Electron
Use Sass / Scss? Y
Select plugins to install > Axios, vue-electron, vue-router, vuex, vuex-electron
Linting with ESLint? > N
Unit testing with Karma + Mocha? N
End to end testing with Spectron + Mocha? N
Which build tool would you like to use? > electron-builder
Author > Paul Halliday

After filling in the above (you can switch some steps out if you want, most of it is personal preference), we have a folder named my-todos with our project inside. Install the dependencies and then open this with your favorite editor:

Note: If you're a Windows user, you may need to follow the steps outlined here prior to continuing.

$ cd my-todos
$ npm install
$ code .

$ npm run dev

If we’ve done everything correctly at this stage, we should be greeted with a macOS application that contains information about our project:

Starter Electron project

Most of our work will be done inside of the src directory, and I’d like to bring your attention to the main and renderer folders.

Main houses index.js and index.dev.js, files that are related to the main process. This is things such as creating a new BrowserWindow with dimensions, listening for app-wide events, and so on.

Renderer is where we keep our Vue application and can be thought of as the directory for our frontend code.

For more information on the Electron architecture and the differences between the main and renderer process, visit this page.

In order to get a feel for how Electron works, let’s visit src/main/index.js and see how our main page is defined:

const winURL = process.env.NODE_ENV === 'development'
  ? `http://localhost:9080`
  : `file://${__dirname}/index.html`

function createWindow () {
  mainWindow = new BrowserWindow({
    height: 563,
    useContentSize: true,
    width: 1000
  })

  mainWindow.loadURL(winURL)

  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)

The BrowserWindow object can be used to display a new browser window (as the name suggests), and we’re using it to open index.html when the application is ready. In turn, this will start our Vue app, giving us the ability to hook into native desktop features.

Now that we know how the Vue application is started, let’s take a look at the defined routes within our application. Head over to src/router/index.js:

export default new Router({
  routes: [
    {
      path: '/',
      name: 'landing-page',
      component: require('@/components/LandingPage').default
    },
    {
      path: '*',
      redirect: '/'
    }
  ]
})

As we can see, the LandingPage component is defined as the default route for our application. We can therefore edit or create a new routes object with our own components in the future.

In order to get Vuex to work with our Electron project, we’ll need to provide a path to our store within src/main/index.js:

import { app, BrowserWindow } from 'electron';
import '../renderer/store';

We can then make a new Store module named Todo.js within src/renderer/store/modules:

import uuid from 'uuid/v4';

const state = {
  todos: []
};

const actions = {
  ADD_TODO ({ commit }, name) {
    console.log(name);
    commit('ADD_TODO', name);
  },
  COMPLETE_TODO ({ commit }, id) {
    commit('COMPLETE_TODO', id);
  },
  CLEAR_TODOS ({ commit }) {
    commit('CLEAR_TODOS');
  }
};

const mutations = {
  ADD_TODO (state, name) {
    state.todos = [ ...state.todos, { id: uuid(), name } ];
  },
  COMPLETE_TODO (state, id) {
    state.todos = state.todos.filter((todo) => todo.id != id);
  },
  CLEAR_TODOS (state) {
    state.todos = [];
  }
};

export default {
  state,
  actions,
  mutations
};

We’re using the third party uuid module to generate new IDs for each Todo. Install that via npm:

$ npm install uuid

Finally, we can edit our LandingPage.vue component to include our small Todo list:

<template>
  <div class="container">
    <div>
      <input
        class="todo-input"
        type="text"
        v-model="todoItemName"
        placeholder="What are you doing today?"
        @keyup.enter.prevent="addTodo"
      >
    </div>
    <div class="todos">
      <ul>
        <li
          class="todo-item"
          v-for="todo in todos"
          :key="todo.id"
          @click="completeTodo(todo)"
        ></li>
      </ul>
    </div>

    <button class="clear-all" @click="clearTodos" v-if="todos.length > 0">CLEAR ALL</button>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  data() {
    return {
      todoItemName: ""
    };
  },
  methods: {
    addTodo() {
      this.$store.dispatch("ADD_TODO", this.todoItemName);

      this.todoItemName = "";
    },
    clearTodos() {
      this.$store.dispatch("CLEAR_TODOS");
    },
    completeTodo(selectedTodo) {
      this.$store.dispatch("COMPLETE_TODO", selectedTodo.id);
    }
  },
  computed: {
    ...mapState({
      todos: state => state.Todo.todos
    })
  }
};
</script>

<style>
.container {
  height: 100vh;
  text-align: center;
  background-color: #30336b;
}

.todos {
  overflow: scroll;
  height: 70vh;
  margin-top: 20px;
}

.todo-input {
  font-size: 36px;
  width: 90vw;
  border: 0px;
  outline: none;
  padding-top: 20px;
  text-align: center;
  background-color: transparent;
  color: white;
}

.todo-item {
  font-size: 24px;
  padding: 10px 0px;
  color: white;
}

.clear-all {
  border: 1px solid white;
  background: transparent;
  color: white;
  margin-top: 20px;
  padding: 20px;
  font-size: 18px;
}

::placeholder {
  color: white;
}
</style>

As this is a demonstration app, I’ve decided to not componentize this further. This finally gives us the following:

Our Electron todo app

Building our app

As we’re using electron-builder to build our application, we can run the following:

$ npm run build

If we look in our package.json, we can also see that we have a variety of other commands at our disposal:

$ npm run build:dir # - this builds an app without installer
$ npm run build:clear # - deletes all builds from the build folder
$ npm run build:web # - builds for web platform

This can be further customized inside of package.json by editing the following object:

"build": {
  "productName": "my-todos",
  "appId": "com.example.yourapp",
  "directories": {
    "output": "build"
  },
  "files": [
    "dist/electron/**/*"
  ],
  "dmg": {
    "contents": [
      {
        "x": 410,
        "y": 150,
        "type": "link",
        "path": "/Applications"
      },
      {
        "x": 130,
        "y": 150,
        "type": "file"
      }
    ]
  },
  "mac": {
    "icon": "build/icons/icon.icns"
  },
  "win": {
    "icon": "build/icons/icon.ico"
  },
  "linux": {
    "icon": "build/icons"
  }
},

The results of our built application can be found in the build/mac or build/platform folder.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#