DEV Community

Cover image for Writing a Firefox Web Extension using Vue.js
Itachi Uchiha
Itachi Uchiha

Posted on

Writing a Firefox Web Extension using Vue.js

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

vue create todo-list-extension
Enter fullscreen mode Exit fullscreen mode

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;

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

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

export default {
  name: 'app',
  components: {
    TodoList
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

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"

TodoList.vue

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

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()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

//....
"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build --no-clean",
    "lint": "vue-cli-service lint"
}
//....
Enter fullscreen mode Exit fullscreen mode

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.

yarn build
Enter fullscreen mode Exit fullscreen mode

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;

{
    "manifest_version": 2,
    "name": "To Do List",
    "version": "1.0.0",
    "chrome_url_overrides" : {
        "newtab": "index.html"
    }
}
Enter fullscreen mode Exit fullscreen mode

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;

about:debugging
Enter fullscreen mode Exit fullscreen mode

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

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.

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

Let's open a new tab :)

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.

Latest comments (3)

Collapse
 
tazim404 profile image
Tazim Rahbar

But how we can make it appear on half window not on new tab
Image

Collapse
 
fasterinnerlooper profile image
Shafiq Jetha • Edited

use "default_popup" like this:
{
"manifest_version": 2,
"name": "MyExtension",
"version": "1.0.0",
"icons": {
"48": "assets/img/logo.png"
},
"browser_action": {
"default_icon": "assets/img/logo.png",
"default_title": "Opens MyExtension",
"default_popup": "app.html"
}
}

Source: javascript.plainenglish.io/i-built...

Collapse
 
arjunauop profile image
Arjuna Bandara

thanks for the awesome tutorial :)