How to create a Chrome extension: Memo Notepad

11/10/2019

JavaScript

After I completed a JavaScript tutorial for beginners which was on YouTube, I was thinking of what to do next. I knew could just add animations on my web service project, but I was more interested in creating an app from scratch with JavaScript. So I decided to create a Chrome extension that can mainly be built with JS.

What I build was a type of memo notepad which is specially made for language learners. Users can create a list of their new vocabulary, and specify the category, tag, and meaning to each vocabulary.

In this article, I am going to explain step by step how I created it.

Create mockups

Firstly, I needed to decide what project I’m going to create, and how it is going to work.

I created mockups with Adobe XD, while I was sort of know what I wanted to make, but wasn’t sure how it’s going to be structured.

This time, I created just a single page with two nav drawers — like in the picture above.

One nav drawer for a form to add new words on the main page. (On the right in the pic)

And the other one to select categories, add new tags and categories. If you select a category, only words which sorted by the category will be shown on the list. Add tag and add category sections are forms. (In the middle of the pic)

On the main page, I want to make the words list with an edit button to every single word. The edit button will open the add-word drawer which will be filled with selected words information. (On the left in the pic)

Idea of how it works

There are two options to store data for chrome extensions, which are local storage and synced storage. This time, it didn’t need to be synced to other devices, so I used local storage.

In the local storage of my extension, there are four arrays — tagData for tag types, catData for category types, wordInfo for all the vocabularies with its related information, and countId for increment numbers for wordInfo id.

tagData:[]
catData:[]
wordInfo:[]
countId:[]

I will explain more about these later in this article.

File structure

Chrome extension’s file structure is pretty simple. It’s like building a single web page but in a small window.

Here is file structure of my project:

popup.html
manifest.json
style.css
load.js
save.js
favicon.png

That’s it! Very minimum, isn’t it? I have separated JS files but it’s not necessary.

Create manifest.json

Now I’m going to create manifest.json. This file is to explain what is your chrome extension is, and how it’s going to be structured.

{
  "name": "Vocabulary Notebook",
  "description": "Simple memo specialized in vocabulary. Create your own vocabulary list.",
  "author": "Maiko Miyazaki",
  "version": "0.1.2",
  "manifest_version": 2,
  "browser_action":{
    "default_title": "Vocabulary Notebook",
    "default_popup": "popup.html"
  },
  "icons": {
    "16": "favicon.png"
  },
  "permissions": [
    "activeTab",
    "storage"
  ]
}

For more information, you can follow Chrome Extension official tutorial for developing your own json files.

Create popup.html

popup.html will be the default popup page when users click the icon of the extension. There are three sections in my HTML file — add-word-drawer area, tag-category-drawer area, and main area.

Here are snippets of my HTML file where it’s going to send and receive data from local storage.

What is local storage and synced storage?

main area

<main>
  <ul class="words-ul" id="words-ul">
    <!--  Data in wordInfo will be listed here. -->
  </ul>
</main>

add-word-drawer area

<form class="add-word-form" id="add-word-form">

  <label for="vocabulary" class="content-titles">Vocabulary</label>
  <input id="vocabulary" class="word-input" type="text">

  <p class="content-titles">Tags</p>
  <div class="tag-container" id="tag-container">
    <!-- data in tagData will be shown here. -->
  </div>

  <div class="meaning-container">
    <p class="content-titles">Meaning</p>
    <textarea name="meaning" id="meaning-textarea" cols="30" rows="5"></textarea>
  </div>

  <p class="content-titles">Category</p>
  <div class="category-container" id="category-container">
    <!-- data in catData will be shown here. -->
  </div>

  <!--  When this form is opened by edit button, id-sender send id of edited item. -->
  <input type="hidden" id="id-sender">

  <button class="add-word-btn add-word-submit" type="submit">Submit</button>

</form>

<form id="cancel-form">
  <button id="cancel-btn" type="submit">Cancel</button>
</form>

Make sure to name id to each input and textarea. They are going to be used for specifying with JavaScript.

tag-category-drawer area

<div class="content-container">

  <div id="nav-cat-container">
    <big>Category</big>
    <div>
      <form class="cat-form">
        <button type="submit" id="submit-1" class="category-btn">All</button>
      </form>
    </div>
  </div>

  <form class="add-container" id="add-tags">
    <big>Add Tag</big>
    <div>
      <input class="tag-input" id="tag-input" type="text">
      <button type="submit">Submit</button>
    </div>
  </form>

  <form class="add-container" id="add-category">
    <big>Add Category</big>
    <div>
      <input class="category-input" id="category-input" type="text">
      <button type="submit">Submit</button>
    </div>
  </form>

</div><!-- .content-container -->

Create save.js

All the actions that save data are put together in this file.

Add-tag section, Add-category section

I’ll make a function to add a tag to tagData from a form. Add-category section works exactly the same as this Add-tag section — just change the name of the form and array.

if ( document.getElementById('add-tags') ) {
    document.getElementById('add-tags').onsubmit = () => {
        let newTag = document.getElementById('tag-input').value;
        // get tagData from storage to not to overwrite the data.
        chrome.storage.local.get({ tagData:[] }, function(items) {
            // Varidation not to send empty value.
            if ( newTag == '' ) {
                alert('Please fill the form.')
                return false
            }
            // Push to the array if it returns -1 (which means newTag does not already exist in the tagData.)
            if (items.tagData.indexOf(newTag) == -1 ) {
                items.tagData.push(newTag);
                chrome.storage.local.set(items);
            } else {
                alert('This tag already exists.')
            }
        });
    }
}

In this code, if users submit (‘add-tags’) form, the value of the input will be pushed to the tagData array.

Add-word section

Same perspective to the add-tag section and add-category section. When users submit (‘add-word-form’), the value of the input will be pushed into the wordInfo array.

But I want to bundle all the information into a variable, like this:

Associative arrays make it possible for this situation.

if ( document.getElementById('add-word-form') ) {
    document.getElementById('add-word-form').onsubmit = () => {
	      // get all the information from storage
        chrome.storage.local.get(null, function(items) {
            // Add 1 to countId for the first submit
            if (items.countId === undefined || items.countId === 0) {
                let id = 1
                items.countId = id;
                chrome.storage.local.set(items);
            // increment countId
            } else {
                items.countId++
                chrome.storage.local.set(items);
            }

            // initiallize wordInfo array
            if(!items.wordInfo){
                items.wordInfo = [];
            }
            // initiallize associative array which stores all the information of a vocabulary.
            const newWord = { id:'', word:'', tag:[], category:'', meanings:'' };

            // Add countId to newWord.id 
            const cId = items.countId;
            newWord.id = cId;

            // Add value of the form to newWord.word
            let vocab = document.getElementById('vocabulary').value;
            newWord.word = vocab;

            // Add value of the form to newWord.meanings
            let meaning = document.getElementById('meaning-textarea').value;
            newWord.meanings = meaning;

            // Add all checked tags to newWord.tag
            let tagEl = document.getElementById('tag-container');
            let tags = tagEl.getElementsByTagName('input');

            // Find which tags are checked
            for (let i=0, len=tags.length; i<len; i++ ) {
                if (tags[i].checked) {
                    let checkedLabel = document.getElementById(`tag${i}-label`).textContent;
                    // Push tag not to overwrite previous tag
                    newWord.tag.push(checkedLabel);
                }
            }

            // Add cecked category to newWord.category
            let catEl = document.getElementById('category-container');
            let cat = catEl.getElementsByTagName('input');

            // Find which category is checked
            for (let i=0, len=cat.length; i<len; i++ ) {
                if (cat[i].checked) {
                    let checkedSpan = document.getElementById(`cat${i}-span`).textContent;
                    // Add data to newWord.category
                    newWord.category = checkedSpan;
                }
            }

            // If it's the first item in wordInfo
            if(items.wordInfo === undefined ) {
                items.wordInfo[0] = newWord;
            // Else add new item in the beginning of the list.
            } else {
                items.wordInfo.unshift(newWord); 
            }

            // Set the data into the storage.
            chrome.storage.local.set(items);

        });

    }

}

Create load.js

Display Tag data

All the functions that display data are put together in this file.

These tags are stored data in tagData. To extract data and display them, use chrome.storage.local.get.

chrome.storage.local.get({ tagData:[] }, function(items) {

    let i;
    for( i = 0; i < items.tagData.length; i++ ) {

        // Find where to display data
        const tagContainer = document.getElementById('tag-container')
        const tagDiv = document.createElement('div')

        // Create input for tags
        const tagInput = document.createElement('input')
        // Set unique id
        tagInput.setAttribute('id', `tag${i}-input`)
        tagInput.setAttribute('type', 'checkbox')

        // Create label for the input
        const tagLabel = document.createElement('label')
        tagLabel.textContent = items.tagData[i];
        tagLabel.setAttribute('for', `tag${i}-input`)
        tagLabel.setAttribute('id', `tag${i}-label`)

        // Display them inside tagDiv
        tagContainer.appendChild(tagDiv)
        tagDiv.appendChild(tagInput)
        tagDiv.appendChild(tagLabel)
            
    }

});

Display Category data

Same ideas as displaying tag data. Category data need to be displayed in a couple of areas — in add-word-drawer and tag-category-drawer.(Picture above)

chrome.storage.local.get({ catData:[] }, function(items) {

    let i;
    for( i = 0; i < items.catData.length; i++ ) {
        // Display in add-word-drawer
        // Find where to display
        const catContainer = document.getElementById('category-container')

        // Create input
        const catLabel = document.createElement('label')
        const catInput = document.createElement('input')
        catInput.setAttribute('type', 'radio')
        // Set unique id
        catInput.setAttribute('id', `cat${i}-input`)
        catInput.setAttribute('name', 'category')

        // Display data as textContent
        const catSpan = document.createElement('span')
        catSpan.textContent = items.catData[i];
        catSpan.setAttribute('id', `cat${i}-span`)

        // Display it inside catContainer
        catContainer.appendChild(catLabel)
        catLabel.appendChild(catInput)
        catLabel.appendChild(catSpan)

        // Display in add-tag-category-drawer
        // Find where to display
        const navContainer = document.getElementById('nav-cat-container')

        // Create form
        const navDiv = document.createElement('div')
        const navForm = document.createElement('form')
        navForm.setAttribute('id', `cat-form${i}`)
        navForm.setAttribute('class', 'cat-form')        
        const navBtn = document.createElement('button')
        navBtn.setAttribute('type', 'submit')
        // Create unique id
        navBtn.setAttribute('id', `submit${i}`)
        navBtn.setAttribute('class', 'category-btn')
        // Set data as textContent
        navBtn.textContent = items.catData[i];

        // Display it inside navDiv
        navContainer.appendChild(navDiv)
        navDiv.appendChild(navForm)
        navForm.appendChild(navBtn)

        // Close nav bar when clicking category btn
        const input = document.getElementById('nav-input')
        const hmbBtn = document.getElementById('nav-open')
        hmbBtn.addEventListener('click', function() {
            input.setAttribute('class', 'nav-input');
          });
        navBtn.addEventListener('click', function() {
            input.removeAttribute('class')
            input.checked = false
          });
    }

});

Create a function to display data of wordInfo

function displayWords(root, location, storage) {

    for( let i = 0; i < storage.length; i++ ) {
        // Build main-area with words
        // Main <ul> 
        const wordUl = document.getElementById('words-ul')
        // Message if there's no data
        const message = document.createElement('p')
        message.textContent = ('Please add tags from side bar.')
        
        if ( storage === undefined ) {
            wordUl.appendChild(message)   
        }
        // <li>
        const wordLi = document.createElement('li')
        wordLi.setAttribute('class', 'flexbox')

        // First <p> word
        const wordPara1 = document.createElement('p')
        wordPara1.setAttribute('class', 'main-word')
        wordPara1.setAttribute('id', `main-word-${i}`)
        wordPara1.textContent = storage[i].word
        // <div> for tag's <span>
        const tagDiv = document.createElement('div')
        tagDiv.setAttribute('class', 'tagDiv')
        tagDiv.setAttribute('id', `tagDiv${i}`)

        wordUl.appendChild(wordLi)
        wordLi.appendChild(wordPara1)
        wordLi.appendChild(tagDiv)

        // Tag's <span>
        for (let j = 0; j < storage[i].tag.length; j++ ) {

            const divTag = document.getElementById(`tagDiv${i}`)
            const tagSpan = document.createElement('span')
            tagSpan.textContent = storage[i].tag[j]
            tagSpan.setAttribute('class', 'main-tag');
            divTag.appendChild(tagSpan)

        }
        // Second <p> meaning
        const wordPara2 = document.createElement('p')
        wordPara2.textContent = storage[i].meanings
        wordPara2.setAttribute('class', 'main-meaning')

        wordLi.appendChild(wordPara2)

        // Edit button
        const editForm = document.createElement('form')
        editForm.setAttribute('id',`edit-submit-form${i}`)
        
        const editSubmit = document.createElement('button')
        editSubmit.setAttribute('type', 'submit')
        editSubmit.setAttribute('class', 'edit-submit-btn')

        const fontAwesome = document.createElement('i')

        wordLi.appendChild(editForm)
        editForm.appendChild(editSubmit)
        editSubmit.appendChild(fontAwesome)

        wordLi.addEventListener('mouseenter', () => {
            fontAwesome.setAttribute('class', 'fas fa-edit')
        }, false);

        wordLi.addEventListener('mouseleave', () => {
            fontAwesome.removeAttribute('class')
        }, false);

        // Pull down word-edit form
        document.getElementById(`edit-submit-form${i}`).onsubmit = () => {
            
            const input = document.getElementById('add-word-input')
            input.checked = true;
            // Change ID so the form can send different way
            const editWordForm = document.getElementById('add-word-form')
            editWordForm.id = 'edit-word-form'
            // Display data related to the button that is submitted
            //// ID
            document.getElementById('id-sender').value = storage[i].id
            //// word
            document.getElementById('vocabulary').value = storage[i].word
            //// meaning
            document.getElementById('meaning-textarea').value = storage[i].meanings
            //// tag
            chrome.storage.local.get({ tagData:[] }, function(items) {

                for (let k = 0; k < items.tagData.length; k++ ) {

                    const a = items.tagData[k]
                    
                    for (let l = 0; l < storage[i].tag.length; l++ ) {

                        const b = storage[i].tag[l]

                        if ( a === b ) {
                            document.getElementById(`tag${k}-input`).checked = true
                        }

                    }
                    
                }
            });
            //// category
            chrome.storage.local.get({ catData:[] }, function(items) {

                for (let k = 0; k < items.catData.length; k++ ) {

                    const a = items.catData[k]

                    const b = storage[i].category

                    if ( a === b ) {
                        document.getElementById(`cat${k}-input`).checked = true
                    }

                    
                }
            });

            document.getElementById('cancel-form').onsubmit = () => {

                if ( document.getElementById('edit-word-form') ) {
                    document.getElementById('edit-word-form').id = 'add-word-form'
                    // Close the pull down window
                    input.checked = false;
                } else {
                    input.checked = false;
                }

            }

            // Set data into storage.local
            document.getElementById('edit-word-form').onsubmit = () => {

                let editedWord = { id:'', word:'', tag:[], category:'', meanings:'' };

                editedWord.id = document.getElementById('id-sender').value
                editedWord.word = document.getElementById('vocabulary').value
                editedWord.meanings = document.getElementById('meaning-textarea').value

                // Push tag
                const tagEl = document.getElementById('tag-container');
                const tags = tagEl.getElementsByTagName('input');
                for (let l=0, len=tags.length; l<len; l++ ) {
            
                    if (tags[l].checked) {
        
                        const nTag = document.getElementById(`tag${l}-label`).textContent;
                        editedWord.tag.push(nTag);
        
                    }
        
                }

                // Push category
                const catEl = document.getElementById('category-container');
                const cat = catEl.getElementsByTagName('input');
                for (let m=0, len=cat.length; m<len; m++ ) {
                    
                    if (cat[m].checked) {

                        let checkedSpan = document.getElementById(`cat${m}-span`).textContent;
                        editedWord.category = checkedSpan;

                    }

                }

                for (let n = 0; n < location.length; n++ ) {
                    if ( location[n].id == editedWord.id ) {
                        location[n] = editedWord
                        chrome.storage.local.set(root)
                        alert('Successfully edited!')
                    } 
                }
                
                const putBackWordForm = document.getElementById('edit-word-form')
                putBackWordForm.id = 'add-word-form'

            }
            return false;
  
        };


    }

}

This part is pretty long — even though functions are supposed to be separated, and should be as short as possible, to make it easier to manage.

As it was a project of mine that was made just after I finished the basic tutorial of JavaScript, it still needs to be improved.

Display all the data of wordInfo as default

chrome.storage.local.get({ wordInfo:[] }, function(items) {

    displayWords(items, items.wordInfo, items.wordInfo);

});

Display chosen words by category

chrome.storage.local.get({ catData:[] }, function(items) {
    
    for( let h = 0; h < items.catData.length; h++ ) {

        // Word list sorted by Category
        document.getElementById(`cat-form${h}`).onsubmit = function(){

            const removeItems = document.getElementById('words-ul')
            while (removeItems.firstChild) {
                removeItems.removeChild(removeItems.firstChild)
            }

            chrome.storage.local.get({ wordInfo:[] }, function(items) {

                const keyword = document.getElementById(`submit${h}`).textContent

                const allItems = items.wordInfo

                function filterByCategory(item) {

                    if ( item.category === keyword ) {
                        return true
                    }
                    
                }

                let result = allItems.filter(filterByCategory)

                displayWords(items, items.wordInfo, result);
            });
                
            return false
        };
    
    }

});

To see the full script and markups -> https://github.com/miyazakimaiko/Chrome-extention

Reference