Writing a Firefox Web Extension using Vue.js
Tags: javascript, vue, tutorial
Published: 2019-04-02 18:55:58

Hi, It's been a long time since I was published my last tutorial post.

In this post, I'll explain how to write a web extension for Firefox using Vue.js.

Before starting, I'm sorry for my grammar mistakes.

In this example, we will create a to-do list app by overriding the browser's new tab.

## Pre Requirements

You need to have knowledge about Vue to understand this post. But you don't need to Vue to create an extension like this. You can create one for yourself using Vanilla JS.

## Creating Vue Project

```bash
vue create todo-list-extension
```

The name doesn't matter. I just like meaningful names. We will not use vuex or router. We will use localStorage as a database.

## Replacing Default Component.

I'll replace the default component under the `/src/components/` as TodoList. You also need to change its name in the `App.vue`

### App.vue

App.vue should be like this;

```vue
<template>
  <div>
    <todo-list />
  </div>
</template>

<script>
import TodoList from './components/TodoList.vue'
import './components/TodoList.css'

export default {
  name: 'app',
  components: {
    TodoList
  }
}
</script>
```

### TodoList.css

I created a CSS file named TodoList.css in the components directory. The CSS will be like this. You can find this CSS if you googled for "Todo MVC"

{% gist https://gist.github.com/aligoren/da61c4b79b6b5dd74ecad538be49f619 %}

### TodoList.vue

Now we will create our application. Firstly, the `template` will be like this;

```vue
<template>
  <div>
    <section class="todoapp">
      <header class="header">
        <h1>To Do List</h1>
        <input class="new-todo"
          autofocus autocomplete="off"
          placeholder="What needs to be done?"
          v-model="newTodo"
          @keyup.enter="addTodo">
      </header>
      <section class="main" v-show="todos.length" >
        <input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
        <label for="toggle-all"></label>
        <ul class="todo-list">
          <li v-for="todo in filteredTodos"
            class="todo"
            :key="todo.id"
            :class="{ completed: todo.completed, editing: todo == editedTodo }">
            <div class="view">
              <input class="toggle" type="checkbox" v-model="todo.completed">
              <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
              <button class="destroy" @click="removeTodo(todo)"></button>
            </div>
            <input class="edit" type="text"
              v-model="todo.title" v-todo-focus="todo == editedTodo"
              @blur="doneEdit(todo)"
              @keyup.enter="doneEdit(todo)"
              @keyup.esc="cancelEdit(todo)" />
          </li>
        </ul>
      </section>
      <footer class="footer" v-show="todos.length">
        <span class="todo-count">
          <strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
        </span>
        <ul class="filters">
          <li><a href="#" @click="filterTodos('all')" :class="{ selected: visibility == 'all' }">All</a></li>
          <li><a href="#" @click="filterTodos('active')" :class="{ selected: visibility == 'active' }">Active</a></li>
          <li><a href="#" @click="filterTodos('completed')" :class="{ selected: visibility == 'completed' }">Completed</a></li>
        </ul>
        <button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
          Clear completed
        </button>
      </footer>
    </section>
    <footer class="info">
      <p>Double-click to edit a todo</p>
      <p>Written by <a href="http://evanyou.me">Evan You</a></p>
      <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>
  </div>
</template>
```

After that, the component's script will be like this;

```javascript
export default {
	name: 'TodoList',
	data() {
		return {
			newTodo: null,
			todos: [],
			filteredTodos: [],
			visibility: 'all',
			editedTodo: null,
			STORAGE_KEY: 'todo-list-v2'
		}
	},
	computed: {
		remaining: function() {
			return this.todos.filter(todo => !todo.completed).length
		},
		allDone: {
			get: function() {
				return this.remaining === 0
			},
			set: function(value) {
				this.todos.map(todo => todo.completed = value)
				
				this.listTodos()
			}
		}
	},
	mounted() {
		
		this.todos = JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []

		this.listTodos()
	},
	methods: {
		listTodos() {

			this.filteredTodos = []

			if (this.visibility == 'all') {
				this.todos.forEach(todo => {
					this.filteredTodos.push(todo)
				})
			} else if(this.visibility == 'active') {
				this.todos.filter(todo => !todo.completed).forEach(todo => {
					this.filteredTodos.push(todo)
				})
			} else if(this.visibility == 'completed') {
				this.todos.filter(todo => todo.completed).forEach(todo => {
					this.filteredTodos.push(todo)
				})
			}
		},
		addTodo() {
			this.todos.push({
				id: this.todos.length + 1,
				title: this.newTodo,
				completed: false
			})

			localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))

			this.listTodos()

			this.newTodo = null
		},
		editTodo(todo) {
			this.editedTodo = todo
		},
		removeTodo(data) {
			this.todos = this.todos.filter(todo => todo.id != data.id)
			
			localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))
			
			this.listTodos()
		},
		doneEdit() {
			
			localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))
			
			this.editedTodo = null
		},
		cancelEdit() {
			this.editedTodo = null
		},
		removeCompleted() {
			this.todos = this.todos.filter(todo => !todo.completed)

			localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))

			this.listTodos()
		},
		filterTodos(type) {
			
			this.visibility = type

			this.listTodos()
		}
	},
	filters: {
		pluralize: function (n) {
			if (n <= 0) {
				return 'item'
			} else if(n === 1) {
				return 'item'
			}

			return n === 1 ? 'item' : 'items'
		}
	},
	directives: {
		'todo-focus': function (el, binding) {
			if (binding.value) {
				el.focus()
			}
		}
	}
}
```

Actually, you can find a lot of example on Google for Todo MVC. This example one of these. So, I will not explain how methods work, what are the directives, filters or computed properties.

## Building Vue Application

If you used Vue in your projects, you should know, Vue project's default output folder is `dist` folder. 

By default, after `yarn build` command, the dist folder removes and re-creates To prevent this, we need to change the package.json file's script section like that.

```json
//....
"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build --no-clean",
    "lint": "vue-cli-service lint"
}
//....
```

We just added the **--no-clean** flag for build operations. I'm doing this because I wanted to use the dist folder for this post.

```bash
yarn build
```

With this command, we've built our todo app.

### manifest.json file

We'll create **manifest.json** file in the `dist` folder. Its content will be like this;

```json
{
    "manifest_version": 2,
    "name": "To Do List",
    "version": "1.0.0",
    "chrome_url_overrides" : {
        "newtab": "index.html"
    }
}
```

Using **chrome_url_overrides** key, you can override browser's default behavior for the new tab feature. That doesn't do that directly, it has subkey to do that. If you use **newtab** key, you can do it.

### Debugging Web Extension

Okay, we finished everything, now let's open a new tab in our browser and typing this command to the address bar;

```text
about:debugging
```

If you use any web extension, you will see them here.

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

If you see the **Load Temporary Add-on..." button, click it. It will open a file dialog. We need to select the manifest.json file we created.

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

If you didn't see any error, we'll see our extension in the extension dashboard.

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

Let's open a new tab :)

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

## Conclusion

- We learned how to write a basic Web Extension app for Firefox using Vue.js

- We learned **chrome_url_overrides** key can be used by Firefox.

- We learned if we want to override new tab we have to use **chrome_url_overrides** and **newtab** key.

Thanks for reading. I hope this helps you.