Please note: this article is written for Manifest v2 which is deprecated. If you wish to publish an extension to the Chrome store you will need to use Manifest v3. How to migrate from Manifest v2 to v3
Chromium browsers are web browsers that are built on top of the open-source Chromium engine. These browsers include Chrome, Brave and Edge.
A Chromium Extension is a web extension built for and that is capable of running on a Chromium browser. This means you can build an extension that not only works for Chrome; which as of May 2022, has a 64.91 global market share; but also for any browsers that run on it's engine.
Chromium extensions are essentially just Javascript scripts as well HTML pages for the extension dialog and settings page.
Chromium extensions consist of the following:
- Manifest.json
- Content Script (JS)
- Background Script (JS)
- Extension Dialog (HTML + JS)
- Settings Page (HTML + JS)
The manifest.json file is a configuration file used by the Chromium Engine to retrieve some information about your extension such as:
- Name
- Permissions
- Registered scripts and pages
Content scripts are Javascript scripts that get injected into the webpage you are currently viewing. From these scripts you can execute code that alters the styling and/or values of HTML elements on the current page.
Content scripts are rather limited in which Chrome APIs they can access. They are pretty much limited to sending and receiving messages, accessing the url of the current page and working with the storage of the current page.
Code can be executed once the content script is loaded or you can use a publish/subscribe pattern with messages to trigger certain logic when:
- Context Menu items are clicked
- Buttons are clicked in the extension dialog page
- Buttons are clicked/values set on the settings page
Background scripts are also written in Javascript and unlike Content Scripts, do not run within the context of the current webpage being viewed.
While background scripts can't interact with the current webpage being viewed, they are able to access a plethora of Chrome APIs (which fire their own events), not limited to, but including:
- Tabs
- Storage (storage for the extension)
- Bookmarks
- Alarms
The type of code that you will write in the Background script is code that will take advantage of the Chrome APIs (creating alarms, reacting to bookmarks being created etc.) and registering Context Menu items if your extension require any.
Communication between Content and Background scripts is done through a publisher/subscriber pattern. In essence, Content and Background scripts fire events/messages that the other listen for, and then execute the appropriate code.
chrome.runtime.onMessage.addListener(message => ...)
can be used to subscribe to incoming messages.
chrome.runtime.sendMessage({ <MESSAGE_DATA> })
can be used to send messages.
The Context Menu is the menu that pops up when right clicking on a webpage. You can create multiple Context Menu items that you can map code to.
Context Menu items should be registered in the Background script as they are an extension wide feature.
Extension pages include the extension dialog, which you can find by right clicking the extension icon in the top right corner of the browser, as well as the settings page which allow you to configure the settings for your extension.
Extensions can store their own data on the browser, allowing you to persist settings.
To work with extension storage, access the storage
Chrome API:
- For reading data:
chrome.storage.sync.get(<STORAGE_KEY>, () => { ... //Callback Function })
- For writing data
chrome.storage.sync.set({ <STORAGE_KEY>: <VALUE> })
The goal: Create a Chrome Extension which can generate a date of birth within the bounds of a minimum and maximum age. These age fields will need to be captured by the user, via the extension dialog, and persisted to the extension's storage.
Start by forking or cloning this repository which is a boilerplate for Chromium extensions, bundled with Webpack: Boilerplate Repository. This repository was originally forked from a repository created by @samuelsimoes.
The repository is structured as follows:
- src
- assets (any image or font assets go here)
- pages (contains pages such as the extension dialog and settings page)
- services (contains any service classes we may create)
- background-script.js
- content-script.js
- manifest.json
- utils (used by Webpack)
- package.json
- webpack.config.js
Run npm run build
in the root of the project to compile the extension. You'll see that a build
file has been created in the root.
To load the extension onto the browser, navigate to your browser's extensions page (eg; chrome://extensions
). Enable developer mode in the top right hand corner and finally click the Load Unpacked
button in the top left corner and select the build
folder on the repository root.
You should see a card for the extension on the extensions page, possibly with a warning informing you that Manifest v2 has been deprecated. If after this article you wish to migrate to Manifest v3, you can read this article
To view the extension dialog, pin the extension in the top right corner of the browser and click on the extension's icon
The Manifest.json contains core information about our extension which is used by the browser such as the extension name
and manifest_version
. You can go ahead and change the name
field if you like.
The permissions
field will be populated later to allow us to access the Chrome storage api.
The background
, content_scripts
and browser_action
(extension dialog) fields are used to register the respective scripts and pages
The content_scripts
field can contain a matches
field which can be used to apply the content script to pages at a specific url. In fact, the content_scripts
field is an array which means you could register multiple content_scripts
and inject them into pages at specific urls.
We'll be doing this on the extension dialog page. Firstly, navigate to the src/pages/extension-dialog-page/extension-dialog-page.html
file.
Start by creating the inputs for the age fields, replacing everything except the script
element inside the body
, assigning specific ids to them so that we can select them easily through Javascript.
<h4>Date of Birth Settings</h4>
<div style="margin-bottom: 10px">
<label for="minimum-age-input">Minimum Age</label>
<input id="minimum-age-input" type="number">
</div>
<div>
<label for="maximum-age-input">Maximum Age</label>
<input id="maximum-age-input" type="number">
</div>
Next, add a Save button beneath the inputs:
<button id="save-button">Save</button>
To view the updated extension dialog: re-run the build
script, navigate back to chrome://extensions
and click on the reload button on the extension's card. Open the extension's dialog, you should see the two inputs and the save button.
We will be using the storage
api to persist the max and min age values. In order to use the storage
api we will need to register a permission. Navigate to the manifest.json
and under permissions
add storage
.
"permissions": ["storage"]
Next, navigate to the extension-dialog-page.js
file and replace the file contents with an enum mapping the id numbers of all important elements on the page.
Putting these ids in an enum instead of hardcoding them in multiple places lowers the chance of mispelling the id and also means that we don't need to remember or go back to the HTML to see what the ids are.
const PAGE_ELEMENT_ID = {
MINIMUM_AGE_INPUT: 'minimum-age-input',
MAXIMUM_AGE_INPUT: 'maximum-age-input',
SAVE_BUTTON: 'save-button'
}
Create two functions, initializePage()
and initializeAgeInputs()
. Inside of initializePage()
, call initializeAgeInputs()
function initializePage() {
initializeAgeInputs();
}
function intializeAgeInputs() {}
Inside of initializeAgeInputs
we will call the get
method on the chrome.storage.sync
api to get two fields in storage. You can put these keys inside of an enum called STORAGE_KEY
at the top of the file or in another file and import it.
export const STORAGE_KEY = {
MINIMUM_AGE: 'minimumAge',
MAXIMUM_AGE: 'maximumAge'
}
function initializeAgeInputs() {
chrome.storage.sync.get(STORAGE_KEY.MINIMUM_AGE, result => {
document.getElementById(PAGE_ELEMENT_ID.MINIMUM_AGE_INPUT).value = result[STORAGE_KEY.MINIMUM_AGE];
});
chrome.storage.sync.get(STORAGE_KEY.MAXIMUM_AGE, result => {
document.getElementById(PAGE_ELEMENT_ID.MAXIMUM_AGE_INPUT).value = result[STORAGE_KEY.MAXIMUM_AGE];
})
}
Under the enum declaration(s), call the initializePage()
method. This will be the entry point for Javascript on the dialog page.
The age fields will still be blank since there isn't an existing value in storage. To test the age input initialization, add the following code above the initializePage()
call.
chrome.storage.sync.set({ [STORAGE_KEY.MINIMUM_AGE]: 18 });
chrome.storage.sync.set({ [STORAGE_KEY.MAXIMUM_AGE]: 65 });
Re-run the build
script, navigate back to chrome://extensions
and click on the reload button on the extension's card. You should now see the values 18 and 65 in the appropriate inputs.
Remove the two lines to set the min and max age once you are done testing.
Next we will persist the values from the two age inputs when clicking the save button.
Create the below function which retrieves the values in the inputs and then saves them using the Chrome storage
api
function saveAgeRange() {
const minAge = document.getElementById(PAGE_ELEMENT_ID.MINIMUM_AGE_INPUT).value;
const maxAge = document.getElementById(PAGE_ELEMENT_ID.MAXIMUM_AGE_INPUT).value;
chrome.storage.sync.set({ [STORAGE_KEY.MINIMUM_AGE]: minAge });
chrome.storage.sync.set({ [STORAGE_KEY.MAXIMUM_AGE]: maxAge });
alert('Min/Max age has been set');
}
This function needs to be fired when the submit button is clicked. Add this code at the bottom of the initializePage()
function.
document.getElementById(PAGE_ELEMENT_ID.SAVE_BUTTON).addEventListener('click', saveAgeRange);
Build and reload to test.
Next we will be creating a context menu item which will replace an input's value with a random date of birth when selected.
Start by adding the contextMenus
permission in the manifest.json
file to allow our extension to access the contextMenus
Chrome API.
We will be using the moment
package for date manipulation and formatting. Run the following command to install it: npm install moment
.
We will need to two classes to handle context menu items, ContextMenuService
and DateOfBirthContextMenuItem
:
ContextMenuService
will contain logic to create context menu items as well as to facilitate communication between the web page and the context menu.DateOfBirthContextMenuItem
will contain the configuration for the context menu item (label text etc.) as well as the logic which is ran when the context menu item is clicked.
Create the classes:
src/context-menu-items/date-of-birth.context-menu-item.js
import { STORAGE_KEY } from '../enums/storage-key.enum';
import moment from "moment";
export class DateOfBirthContextMenuItem {
constructor() {
this.id = 'dateOfBirth';
this.title = 'Generate Date Of Birth';
this.contexts = ['editable'];
}
onClick(info, tab) {
chrome.storage.sync.get([STORAGE_KEY.MINIMUM_AGE, STORAGE_KEY.MAXIMUM_AGE], result => {
const minAge = result[STORAGE_KEY.MINIMUM_AGE];
const maxAge = result[STORAGE_KEY.MAXIMUM_AGE];
const dateOfBirth = this.generateDateOfBirth(minAge, maxAge);
this.replaceInputText(dateOfBirth);
});
}
replaceInputText(newValue) {
const input = document.activeElement;
input.value = newValue;
input.dispatchEvent(new Event('input', {
view: window,
bubbles: true,
cancelable: true
}));
input.dispatchEvent(new Event('change', {
view: window,
bubbles: true,
cancelable: true
}));
}
generateDateOfBirth(minAge, maxAge) {
const minDateEpoch = new moment().subtract(maxAge, 'years').toDate().getTime();
const maxDateEpoch = new moment().subtract(minAge, 'years').toDate().getTime();
const random = new moment(minDateEpoch + Math.random() * (maxDateEpoch - minDateEpoch));
return random.format('YYYY/MM/DD');
}
}
src/services/context-menu.service.js
import { DateOfBirthContextMenuItem } from "../context-menu-items/date-of-birth.context-menu-item";
export class ContextMenuService {
constructor() {
this.contextMenuItems = [
new DateOfBirthContextMenuItem()
]
}
createMenuItems() {
this.contextMenuItems.forEach(contextMenuItem => {
const item = Object.assign({}, contextMenuItem);
delete item.onClick;
item.onclick = (info, tab) => this.sendOnClickMessage(item.id, tab.id);
chrome.contextMenus.create(item);
});
}
handleMessage(message) {
const applicableMenuItem = this.contextMenuItems.find(x => x.id === message.contextMenuId);
if (applicableMenuItem) applicableMenuItem.onClick(message);
}
sendOnClickMessage(contextMenuId, tabId) {
chrome.tabs.sendMessage(tabId, { contextMenuId });
}
}
Next, we will need to register our context-menu item with the browser. This is an action that only needs to happen once when the extension is loaded on the browser so we will register it in the background-script.js
.
Add the following to the bottom of src/background-script.js
:
import { ContextMenuService } from "./services/context-menu.service";
const contextMenuService = new ContextMenuService();
contextMenuService.createMenuItems();
Since we want the context menu item to affect the HTML on the current web page, we will need code to be executed from the content-script.js
. Replace the contents of src/content-script.js
with the following:
import { ContextMenuService } from "./services/context-menu.service";
chrome.runtime.onMessage.addListener(function (message) {
new ContextMenuService().handleMessage(message);
});
The above code listens for messages/events sent by the context menu item and then calls the handleMessage
function on the ContextMenuService
to execute the appropriate logic.