Categories
Vue

Create a Desktop App with Vue and Electron

Electron is an app framework to let us build desktop apps that are based on web apps.

The apps are cross-platform and are rendered with the Chromium browser engine.

We can use the vue-cli-plugin-electron-builder code generator to build an Electron app based on Vue.js.

In this article, we’ll look at how to build a simple Electron Vue app.

Getting Started

We can get started by creating a Vue.js project.

To do that, we create an empty project folder, go into it, and run:

npx vue create .

Then we follow the instructions to add the items we want.

Next, we run the Vue CLI Plugin Electron Builder generator to add the Electron files to our Vue app.

We run:

vue add electron-builder

to add all the files and settings automatically.

Then to start the dev server, we run:

yarn electron:serve

or

npm run electron:serve

We should see a Window for our app displayed.

The dev console should also be shown.

Now we just have to write our Vue app.

Writing the Code

We write the code like any other Vue app.

In App.vue , we write:

<template>
  <div id="app">
    <form @submit.prevent="add">
      <input type="text" v-model="todo" />
      <input type="submit" value="add" />
    </form>
    <div v-for="(t, i) of todos" :key="t.id">
      {{t.todo}}
      <button @click="remove(i)">remove</button>
    </div>
  </div>
</template>

<script>
import { v4 as uuidv4 } from "uuid";

export default {
  name: "App",
  data() {
    return {
      todo: "",
      todos: [],
    };
  },
  methods: {
    add() {
      this.todos.push({ id: uuidv4(), todo: this.todo });
      this.todo = "";
    },
    remove(index) {
      this.todos.splice(index, 1);
    },
  },
};
</script>

to add a todo list into our app.

We have a form to add the todo list.

Then we have the add method to add an entry to this.todos .

We create unique IDs for each entry with the uuid NPM package, which we install by running:

npm i uuid

And we have the remove to remove the this.todos entry with the given index.

In the form with listen to the submit event with the @submit.prevent directive to also call preventDefault to prevent default submission behavior.

Now we should see our todo app displaying in the window.

Build Our App

To build our app into an executable file, we run:

yarn electron:build

with Yarn or:

npm run electron:build

with NPM.

Native Modules

We can add native modules into our app within the vue.config.js file:

module.exports = {
  pluginOptions: {
    electronBuilder: {
      externals: ['my-native-dep'],
      nodeModulesPath: ['../../node_modules', './node_modules']
    }
  }
}

We can add the node_module paths for location the modules.

externals is for listing names of modules that don’t work with our app.

Web Workers

To add workers, we add them to the vue.config.js file by writing:

const WorkerPlugin = require('worker-plugin')

module.exports = {
  configureWebpack: {
    plugins: [new WorkerPlugin()]
  }
}

We install the worker-plugin package by running:

npm i worker-plugin

Now we can use worker files by writing:

src/worker.js

onmessage = (e) => {
  const { a, b } = e.data;
  const workerResult = +a + +b;
  postMessage(workerResult);
}

src/App.vue

<template>
  <div id="app">
    <form @submit.prevent="send">
      <input type="text" v-model="a" />
      <input type="text" v-model="b" />
      <input type="submit" value="add" />
    </form>
    <p>result: {{result}}</p>
  </div>
</template>

<script>
const worker = new Worker("./worker.js", { type: "module" });

export default {
  name: "App",
  data() {
    return {
      a: 0,
      b: 0,
      result: 0
    };
  },
  mounted() {
    worker.onmessage = this.onMessage;
  },
  methods: {
    send() {
      const { a, b } = this;
      worker.postMessage({ a, b });
    },
    onMessage({data}) {
      this.result = data;
    },
  },
};
</script>

We created a worker that listens to the message event.

The message event is emitted by the worker.postMessage method that we emit in the send method.

send gets the data from what we typed in.

Once the message event is emitted, then we get the data sent from e.data .

Then we compute the result and call postMessage to send the computed result back to App.vue .

App.vue gets the result from the onMessage method.

And we display the result from it.

Conclusion

We can create a desktop app with Vue.js with the vue-cli-plugin-electron-builder generator.

It has support for web workers.

Categories
Vue

Using Firebase in a Vue App with Vuexfire — Unbinding, and Geopoints

The Vuefire library lets us add Firebase database manipulation capabilities right from our Vue app.

In this article, we’ll look at how to use Vuefire and Vuexfire to add support for Cloud Firestore database manipulation into our Vue app.

Unbinding

We can stop syncing the state of a collection or document to our Vuex store with the unbindFirestoreRef method.

For example, we can write:

db.js

import firebase from "firebase/app";
import "firebase/firestore";
export const db = firebase
  .initializeApp({ projectId: "project-id" })
  .firestore();
const { Timestamp, GeoPoint } = firebase.firestore;
export { Timestamp, GeoPoint };

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    books: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindBooksRef: firestoreAction((context) => {
      return context.bindFirestoreRef("books", db.collection("books"));
    }),

    unbindBooksRef: firestoreAction(({ unbindFirestoreRef }) => {
      unbindFirestoreRef("books");
    })
  },
  getters: {
    books: (state) => {
      return state.books;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>{{books}}</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindBooksRef"])
  },
  computed: {
    ...mapGetters(["books"])
  },
  mounted() {
    this.bindBooksRef();
  }
};
</script>

We added the unbindBookRef action which calls the unbindFirestoreRef with the collection name string.

By default, when we unbind a collection, the Vuex store state will reset to its initial value.

unbindFirestoreRef(“books”); is the same as unbindFirestoreRef(“books”, true); .

If we don’t want to state to be reset when we unbind, we can pass in false as the 2nd argument:

unbindFirestoreRef("books", false);

We can also reset the state to the value we want if we pass in a function that returns the value we want to reset to:

unbindFirestoreRef('books', () => [{ title: 'foo' }])

If we reset a document, then it’ll be reset to null :

unbindFirestoreRef('book')

Geopoints

We can save geolocation data to our Firebase documents with the GeoPoint constructor.

This is only available with Cloud Firestore only.

For example, we can use it by writing:

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    cities: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindCitiesRef: firestoreAction((context) => {
      return context.bindFirestoreRef("cities", db.collection("cities"));
    })
  },
  getters: {
    cities: (state) => {
      return state.cities;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>{{cities}}</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import { db, GeoPoint } from "./db";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindCitiesRef"])
  },
  computed: {
    ...mapGetters(["cities"])
  },
  async mounted() {
    this.bindCitiesRef();
    await db.collection("cities").add({
      name: "Paris",
      location: new GeoPoint(48.9, 2.3)
    });
  }
};
</script>

We just call add to add the entry to our Firestore collection.

The GeoPoint constructor returns an object that has the latitude and longitude properties.

These are also the arguments of the constructor.

Since we called the bindCitiesRef method, the collection’s documents are automatically synced with the Vuex Store.

And we called mapGetters to map the cities getter to our component, so we’ll see the documents in our template.

Conclusion

We can unbind from our store and add GeoPoint instances to our collection and see the updates immediately.

Categories
Vue

Using Firebase in a Vue App with Vuexfire — Querying and Replacing Documents

The Vuefire library lets us add Firebase database manipulation capabilities right from our Vue app.

In this article, we’ll look at how to use Vuefire and Vuexfire to add support for Cloud Firestore database manipulation into our Vue app.

Querying the Database

We can query the database and synchronize it with the Vuex store with Vuexfire.

Sorting

To sort the data that we get from the Firebase database, we can call orderBy to sort the data.

db.js

import firebase from "firebase/app";
import "firebase/firestore";
export const db = firebase
  .initializeApp({ projectId: "project-id" })
  .firestore();
const { Timestamp, GeoPoint } = firebase.firestore;
export { Timestamp, GeoPoint };

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    books: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindBooksRef: firestoreAction((context) => {
      return context.bindFirestoreRef(
        "books",
        db.collection("books").orderBy("title", "desc")
      );
    })
  },
  getters: {
    books: (state) => {
      return state.books;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>{{books}}</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindBooksRef"])
  },
  computed: {
    ...mapGetters(["books"])
  },
  mounted() {
    this.bindBooksRef();
  }
};
</script>

In the bindBookRef action, we called the bindFirestoreRef method to bind the books state to our books database collection.

The orderBy method sorts the title field value in descending order.

Then state.books returns an array of books object with title sorted in descending order.

Since we called mapGetters to map the getters to computed properties, we’ll see this displayed in the template.

Filtering

We can also filter items with the where method.

For example, we can write:

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    books: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindBooksRef: firestoreAction((context) => {
      return context.bindFirestoreRef(
        "books",
        db.collection("books").where("wordCount", ">", 200)
      );
    })
  },
  getters: {
    books: (state) => {
      return state.books;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

We called where to only return the documents with wordCount bigger than 200.

Then this is what we’ll see in the states and getters.

Writing to the Database

We have to use the Firebase SDK to write to our Firebase database.

For example, we can write:

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    books: [],
    book: {}
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindBooksRef: firestoreAction((context) => {
      return context.bindFirestoreRef("books", db.collection("books"));
    }),

    updateBook: firestoreAction(async ({ state }, { bookId, title }) => {
      const bookSnapshot = await db.collection("books").doc(bookId).get();
      const book = bookSnapshot.data();
      const newBook = { ...book, title };
      await db.collection("books").doc(bookId).set(newBook);
      console.log("book updated");
    })
  },
  getters: {
    books: (state) => {
      return state.books;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>
    <button @click="updateBook">update book</button>
    <div>{{books}}</div>
  </div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindBooksRef"]),
    updateBook() {
      this.$store.dispatch("updateBook", {
        bookId: "ei4jIGJjcmS7eSRKUxsw",
        title: "baz"
      });
    }
  },
  computed: {
    ...mapGetters(["books"])
  },
  mounted() {
    this.bindBooksRef();
  }
};
</script>

In the Vuex store, we have the updateBook action to let us get the book we want to update by its bookId .

And we want to update the title with it.

To do that, we get the books collection with the collection method.

doc gets the document by ID.

get gets the snapshot of the query result.

Then the data method returns the actual data.

Once we did that, we update the title by creating the newBook object.

And once we did that, we call set to update the document.

In the App component, we called dispatch in the updateBook method to dispatch the updateBook Vuex store action to do the update.

Since sync the store state with the collection with bindFirestoreRef , the updated items should show automatically.

Conclusion

We can update the data by using methods from the Firebase SDK.

Since it’s async, we’ve to put the code in actions.

Also, we can sort and filter items when we do the binding of the collection to the state.

Categories
Vue

Using Firebase in a Vue App with Vuexfire — Adding Data

The Vuefire library lets us add Firebase database manipulation capabilities right from our Vue app.

In this article, we’ll look at how to use Vuefire and Vuexfire to add support for Cloud Firestore database manipulation into our Vue app.

Timestamps

We can add a timestamp to our document with the Timestamp.fromDate method.

This is only available when we use Vuexfire with Cloud Firestore.

For example, we can write:

db.js

import firebase from "firebase/app";
import "firebase/firestore";
export const db = firebase
  .initializeApp({ projectId: "project-id" })
  .firestore();
const { Timestamp, GeoPoint } = firebase.firestore;
export { Timestamp, GeoPoint };

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    events: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindEventsRef: firestoreAction((context) => {
      return context.bindFirestoreRef("events", db.collection("events"));
    })
  },
  getters: {
    events: (state) => {
      return state.events;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>{{events}}</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import { db, Timestamp } from "./db";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindEventsRef"])
  },
  computed: {
    ...mapGetters(["events"])
  },
  async mounted() {
    this.bindEventsRef();
    await db.collection("events").add({
      name: "event",
      date: Timestamp.fromDate(new Date("2029-09-14"))
    });
  }
};
</script>

We call the Timestamp.fromDate method to add the timestamp.

And we called bindEventsRef to sync the events collection with the events state in our store.

We then get the events state with a getter.

Then our events state has something like:

[ { "name": "event", "date": { "seconds": 1884038400, "nanoseconds": 0 } } ]

We can also call toDate to turn the Timestamp object back to a human-readable date:

<template>
  <div>
    <div v-for="e of events" :key="e.id">{{e.date.toDate()}}</div>
  </div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import { db, Timestamp } from "./db";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindEventsRef"])
  },
  computed: {
    ...mapGetters(["events"])
  },
  async mounted() {
    this.bindEventsRef();
    await db.collection("events").add({
      name: "event",
      date: Timestamp.fromDate(new Date("2029-09-14"))
    });
  }
};
</script>

References

We can store references of another document in a document.

For example, we can write:

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    books: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindBooksRef: firestoreAction((context) => {
      return context.bindFirestoreRef("books", db.collection("books"));
    })
  },
  getters: {
    books: (state) => {
      return state.books;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>{{books}}</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import { db } from "./db";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindBooksRef"])
  },
  computed: {
    ...mapGetters(["books"])
  },
  async mounted() {
    this.bindBooksRef();
    await db.collection("books").add({
      title: "foo",
      author: db.collection("authors").doc("james-smith")
    });
  }
};
</script>

We just get the collection with db.collection .

Then with what it returns, we call doc with the ID of the authors document as the argument to reference it.

Conclusion

We can add documents to our Firebase database collections and they’ll be automatically reflected in our Vue app if we bind the Vuex state to our collection.

Categories
Vue

Using Firebase in a Vue App Vuefire and Vuexfire

The Vuefire library lets us add Firebase database manipulation capabilities right from our Vue app.

In this article, we’ll look at how to use Vuefire and Vuexfire to add support for Cloud Firestore database manipulation into our Vue app.

Geopoints

We can add the latitude and longitude of a location with the GeoPoint constructor.

This is only supported with the CloudFirestore.

For example, we can write:

<template>
  <div>
    <button @click="add">add city</button>
    <div v-for="c of cities" :key="c.id">{{c}}</div>
  </div>
</template>
<script>
import { db, GeoPoint } from "./db";
export default {
  data() {
    return {
      cities: []
    };
  },
  mounted() {
    this.$bind("cities", db.collection("cities"));
  },
  methods: {
    async add() {
      await db.collection("cities").add({
        name: "London",
        location: new GeoPoint(51.3, 0)
      });
    }
  }
};
</script>

db.js

import firebase from "firebase/app";
import "firebase/firestore";
export const db = firebase
  .initializeApp({ projectId: "project-id" })
  .firestore();
const { Timestamp, GeoPoint } = firebase.firestore;
export { Timestamp, GeoPoint };

We called the add method with a GeoPoint instance to save the latitude and longitude of a location.

Then we should get something like:

{ "name": "London", "location": { "latitude": 51.3, "longitude": 0 } }

saved.

Timestamps

Also, we can save timestamps with Vuefire.

We use the Timestamp.fromDate method to create our timestamp.

For example, we can write:

<template>
  <div>
    <button @click="add">add event</button>
    <div v-for="e of events" :key="e.id">{{e}}</div>
  </div>
</template>
<script>
import { db, Timestamp } from "./db";
export default {
  data() {
    return {
      events: []
    };
  },
  mounted() {
    this.$bind("events", db.collection("events"));
  },
  methods: {
    async add() {
      await db.collection("events").add({
        name: "event",
        date: Timestamp.fromDate(new Date("2029-07-14"))
      });
    }
  }
};
</script>

We passed in a Date instance to Timestamp.fromDate .

Then we get something like:

{ "name": "event", "date": { "seconds": 1878681600, "nanoseconds": 0 } }

saved as a result.

Vuefire

We can use Vuefire to get and set data from Firebase database and store the database’s state in our Vuex store.

To use it, we install the required packages by running:

yarn add vuex vuexfire vuefire firebase

or

npm i vuex vuexfire vuefire firebase

Now we can create our Vuex store and bind our store to the Firebase database by writing:

main.js

import Vue from "vue";
import App from "./App.vue";
import { firestorePlugin } from "vuefire";
import { vuexfireMutations, firestoreAction } from "vuexfire";
import Vuex from "vuex";
import { db } from "./db";

Vue.use(Vuex);
Vue.use(firestorePlugin);
Vue.config.productionTip = false;

const store = new Vuex.Store({
  state: {
    books: []
  },
  mutations: {
    ...vuexfireMutations
  },
  actions: {
    bindBooksRef: firestoreAction((context) => {
      return context.bindFirestoreRef("books", db.collection("books"));
    })
  },
  getters: {
    books: (state) => {
      return state.books;
    }
  }
});

new Vue({
  store,
  render: (h) => h(App)
}).$mount("#app");

App.vue

<template>
  <div>{{books}}</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";

export default {
  data() {
    return {};
  },
  methods: {
    ...mapActions(["bindBooksRef"])
  },
  computed: {
    ...mapGetters(["books"])
  },
  mounted() {
    this.bindBooksRef();
  }
};
</script>

We create our store with the Vuex.Store constructor as usual.

But we import the vuefireMutations and put it in our store.

Also, our actions are created with the fireStoreAction function.

And we call the context.bindFirestoreRef method to get our store data and set it as the state.

The first argument is the state name, and the 2nd is the collection we want to bind to the state with the given name.

Also, we have a getter to get the books state.

In App.vue , we map the actions and getters to methods and computed properties respectively.

After we done that, we can call this.bindBooksRef action which we mapped to a method to populate the books state.

Then our template should show the books data.

Conclusion

We can add geolocation and timestamp data with Vuefire.

Also, the Vuexfire library lets us bind the Firebase database to our Vuex store easily.