Have you ever used a Chrome extension in your Google Chrome web browser? I personally use a ton of them and some of my favorites include ColorZilla and Google Hangouts. Have you ever wanted to make your own Chrome extension, or maybe more importantly a Chrome extension that synchronizes data with your remote datasource?

Chrome extensions are just JavaScript applications and as JavaScript developers we have all the skills necessary to create something awesome.

We’re going to take a look at creating a Google Chrome extension that synchronizes shopping list data between browsers and even devices using Angular 2, PouchDB, and Couchbase.

The Requirements

There are a few requirements when it comes to making the planned popup extension. They are as follows:

Node.js 4.0+

Angular CLI

Couchbase Sync Gateway 1.3+

Because we’re building the extension using Angular 2, we’ll need the Angular CLI which is made available through the Node Package Manager (NPM) found in Node.js. While we won’t be using Couchbase Server in this example, it can easily be added to our soon to be created Couchbase Sync Gateway configuration file. Instead we’re going to use an in-memory storage solution that comes with Sync Gateway and we’ll be using Sync Gateway to synchronize our data between browsers, devices, platforms, and Couchbase Server.

Preparing Couchbase Sync Gateway for Data Replication

There are several ways to create a Chrome extension that uses Couchbase. For this example, and in an effort to only make one application, we’re going to keep it client facing with JavaScript and Couchbase Sync Gateway. Other examples could include a back-end service which our client application communicates to and that back-end service would communicate with Couchbase.

Since the Chrome extension is an application, we need a configuration for Sync Gateway so it knows how to orchestrate the data. A configuration in one of its simplest forms might look like the following:

{ "log":["CRUD+", "REST+", "Changes+", "Attach+"], "databases": { "example": { "server":"walrus:", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } }, "CORS": { "Origin": ["http://localhost:4200", "chrome-extension://ecfgpjabjmlpkedejekaipjipddddohj"], "LoginOrigin": ["http://localhost:4200", "chrome-extension://ecfgpjabjmlpkedejekaipjipddddohj"], "Headers": ["Content-Type"], "MaxAge": 17280000 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { "log" : [ "CRUD+" , "REST+" , "Changes+" , "Attach+" ] , "databases" : { "example" : { "server" : "walrus:" , "sync" : ` function ( doc ) { channel ( doc . channels ) ; } ` , "users" : { "GUEST" : { "disabled" : false , "admin_channels" : [ "*" ] } } } } , "CORS" : { "Origin" : [ "http://localhost:4200" , "chrome-extension://ecfgpjabjmlpkedejekaipjipddddohj" ] , "LoginOrigin" : [ "http://localhost:4200" , "chrome-extension://ecfgpjabjmlpkedejekaipjipddddohj" ] , "Headers" : [ "Content-Type" ] , "MaxAge" : 17280000 } }

The above configuration would be saved to a file named sync-gateway-config.json and loaded when launching Sync Gateway.

In the configuration we’re using a database called example with no specific read or write permissions. The CORS section is for cross origin resource sharing, a requirement when it comes to JavaScript applications. What we’re saying is we’d like to allow connections from port 4200 which is our Angular 2 application during testing, and the address of our Chrome extension.

Download Sync Gateway and launch it by executing the following:

/path/to/sync_gateway /path/to/sync-gateway-config.json 1 / path / to / sync_gateway / path / to / sync - gateway - config . json

The above will start serving Sync Gateway at http://localhost:4985/_admin/.

Creating a New Application with Bootstrap and Angular 2

For simplicity we’re going to start a new Angular 2 project and later bundle it into a Chrome extension. From the Angular CLI, execute the following command:

ng new AngularProject 1 ng new AngularProject

The above command will create a ready to go Angular 2 project. In an effort to save us from having a horrifying UI, we’re going to include the popular Bootstrap framework. There are plenty of other frameworks, and in one of my previous examples I even used Ionic as the framework.

This is a bit of a manual process since Angular 2 doesn’t necessarily play nice with our dependencies, but we need to download Bootstrap, jQuery, and Bootbox.

Place the JavaScript files in the project’s src/assets/js/ directory, the CSS files in the project’s src/assets/css/ directory, and the fonts files in the project’s src/assets/fonts/ directory.

Bootstrap will be our CSS framework, but it has a dependency on jQuery. Bootbox will give us a convenient way to create popup prompts for our application. It is built on Bootstrap and jQuery.

We need to add Bootstrap to our project the old fashion way. Open the project’s src/index.html file and make it look like the following:

<!doctype html> Couchbase Shopping <script src="assets/js/jquery-3.1.1.min.js"></script><script src="assets/js/bootstrap.min.js"></script><script src="assets/js/bootbox.min.js"></script> Loading... 1 2 3 4 5 6 7 8 9 10 11 < ! doctype html > Couchbase Shopping <script src = "assets/js/jquery-3.1.1.min.js" > </script> <script src = "assets/js/bootstrap.min.js" > </script> <script src = "assets/js/bootbox.min.js" > </script> Loading . . .

Notice we’re importing the CSS and JavaScript files that were downloaded.

At this point in time we can start coding, but before we do, it is a good idea to obtain a few more dependencies that will be used throughout this project.

Using the Command Prompt (Windows) or Terminal (Mac and Linux) execute the following:

npm install pouchdb --save npm install uuid @types/uuid --save npm install @types/node --save 1 2 3 npm install pouchdb -- save npm install uuid @ types / uuid -- save npm install @ types / node -- save

The above commands will install PouchDB which we’ll use to replicate data to and from Couchbase Sync Gateway. We’ll use the UUID dependency for generating unique id values that we’ll use as document keys and we’ll use the Node dependency so we can import the PouchDB library into our project. We need the Node dependency since PouchDB doesn’t have TypeScript type definitions.

Let’s start developing the application behind our extension now.

Developing a Shared Provider with Angular 2 and PouchDB

If you’ve seen my past few tutorials, this might seem familiar to you. When working with data in an Angular 2 application, it is a good idea to segregate the logic from the rest of the application. We can do this by creating a shared Angular 2 provider that wraps PouchDB and can be used throughout the application.

Create a file, src/app/pouchdb.service.ts and include the following TypeScript logic:

import { Injectable, EventEmitter } from '@angular/core'; var PouchDB = require("pouchdb"); @Injectable() export class PouchService { private isInstantiated: boolean; private database: any; private listener: EventEmitter = new EventEmitter(); public constructor() { if(!this.isInstantiated) { this.database = new PouchDB("nraboy"); this.database.changes({ live: true, include_docs: true }).on('change', change => { this.listener.emit(change); }); this.isInstantiated = true; } } public get(id: string) { return this.database.get(id); } public put(document: any, id: string) { document._id = id; return this.get(id).then(result => { document._rev = result._rev; return this.database.put(document); }, error => { if(error.status == "404") { return this.database.put(document); } else { return new Promise((resolve, reject) => { reject(error); }); } }); } public sync(remote: string) { let remoteDatabase = new PouchDB(remote); this.database.sync(remoteDatabase, { live: true, retry: true }); } public getChangeListener() { return this.listener; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import { Injectable , EventEmitter } from '@angular/core' ; var PouchDB = require ( "pouchdb" ) ; @ Injectable ( ) export class PouchService { private isInstantiated : boolean ; private database : any ; private listener : EventEmitter = new EventEmitter ( ) ; public constructor ( ) { if ( ! this . isInstantiated ) { this . database = new PouchDB ( "nraboy" ) ; this . database . changes ( { live : true , include_docs : true } ) . on ( 'change' , change = > { this . listener . emit ( change ) ; } ) ; this . isInstantiated = true ; } } public get ( id : string ) { return this . database . get ( id ) ; } public put ( document : any , id : string ) { document . _id = id ; return this . get ( id ) . then ( result = > { document . _rev = result . _rev ; return this . database . put ( document ) ; } , error = > { if ( error . status == "404" ) { return this . database . put ( document ) ; } else { return new Promise ( ( resolve , reject ) = > { reject ( error ) ; } ) ; } } ) ; } public sync ( remote : string ) { let remoteDatabase = new PouchDB ( remote ) ; this . database . sync ( remoteDatabase , { live : true , retry : true } ) ; } public getChangeListener ( ) { return this . listener ; } }

There is a lot going on so we should probably take a moment to break it down.

import { Injectable, EventEmitter } from '@angular/core'; var PouchDB = require("pouchdb"); 1 2 import { Injectable , EventEmitter } from '@angular/core' ; var PouchDB = require ( "pouchdb" ) ;

We plan to inject this provider in each of our extension pages. Since PouchDB has a change listener, we want to emit those changes so the pages can subscribe to them.

public constructor() { if(!this.isInstantiated) { this.database = new PouchDB("nraboy"); this.database.changes({ live: true, include_docs: true }).on('change', change => { this.listener.emit(change); }); this.isInstantiated = true; } } 1 2 3 4 5 6 7 8 9 10 11 12 public constructor ( ) { if ( ! this . isInstantiated ) { this . database = new PouchDB ( "nraboy" ) ; this . database . changes ( { live : true , include_docs : true } ) . on ( 'change' , change = > { this . listener . emit ( change ) ; } ) ; this . isInstantiated = true ; } }

The goal is to have a single instance of the local database. After opening the database and configuring the change emitters, we want to store record that the database is open so we can’t open it again. This is best done in the constructor method.

Saving to PouchDB isn’t more than a single line, but if we want to upsert, it takes a bit more:

public put(document: any, id: string) { document._id = id; return this.get(id).then(result => { document._rev = result._rev; return this.database.put(document); }, error => { if(error.status == "404") { return this.database.put(document); } else { return new Promise((resolve, reject) => { reject(error); }); } }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public put ( document : any , id : string ) { document . _id = id ; return this . get ( id ) . then ( result = > { document . _rev = result . _rev ; return this . database . put ( document ) ; } , error = > { if ( error . status == "404" ) { return this . database . put ( document ) ; } else { return new Promise ( ( resolve , reject ) = > { reject ( error ) ; } ) ; } } ) ; }

Upserting involves checking to see if the document already exists. If it exists, capture the revision and update it. If the document doesn’t exist, create the document.

public sync(remote: string) { let remoteDatabase = new PouchDB(remote); this.database.sync(remoteDatabase, { live: true, retry: true }); } 1 2 3 4 5 6 7 public sync ( remote : string ) { let remoteDatabase = new PouchDB ( remote ) ; this . database . sync ( remoteDatabase , { live : true , retry : true } ) ; }

We wish to sync so we will create a sync method that takes a remote database. The remote database is our Couchbase Sync Gateway. When activated we will sync continuously in both directions.

public getChangeListener() { return this.listener; } 1 2 3 public getChangeListener ( ) { return this . listener ; }

Finally we have a getChangeListener method which will allow us to retrieve and subscribe to the change event emitter.

Before we can start using the provider, it must be imported into our project’s @NgModule block. Open the project’s src/app/app.module.ts file and include the following:

import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { PouchService } from "./pouchdb.service"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [PouchService], bootstrap: [AppComponent] }) export class AppModule { } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { BrowserModule } from '@angular/platform-browser' ; import { NgModule } from '@angular/core' ; import { FormsModule } from '@angular/forms' ; import { HttpModule } from '@angular/http' ; import { AppComponent } from './app.component' ; import { PouchService } from "./pouchdb.service" ; @ NgModule ( { declarations : [ AppComponent ] , imports : [ BrowserModule , FormsModule , HttpModule ] , providers : [ PouchService ] , bootstrap : [ AppComponent ] } ) export class AppModule { }

Notice that the PouchService was imported and added to the providers array of the @NgModule block. At this point the provider can be used in any page of the extension.

Building the Application and Adding Chrome Extension Data

There are a few things that must be done within this project. The Angular 2 functionality for the extension popup must be added and it must be packaged into an actual Google Chrome extension, rather than left as a web application.

Creating the Angular 2 Application Page

The extension we’re building only consists of a single page. This page will have a custom HTML and CSS interface as well as logic to drive it.

Starting with the TypeScript, open the project’s src/app/app.component.ts file and include the following TypeScript:

import { Component, OnInit, NgZone } from '@angular/core'; import * as Uuid from "uuid"; import { PouchService } from "./pouchdb.service"; declare var bootbox: any; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { public items: Array; public constructor(private database: PouchService, private zone: NgZone) { this.items = []; } public ngOnInit() { this.database.sync("http://localhost:4984/example"); this.database.getChangeListener().subscribe(data => { this.zone.run(() => { this.items.push(data.doc); }); }); } public insert() { bootbox.prompt("What do you want to add?", result => { if(result) { this.database.put({type: "list", title: result}, Uuid.v4()); } }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { Component , OnInit , NgZone } from '@angular/core' ; import * as Uuid from "uuid" ; import { PouchService } from "./pouchdb.service" ; declare var bootbox : any ; @ Component ( { selector : 'app-root' , templateUrl : './app.component.html' , styleUrls : [ './app.component.css' ] } ) export class AppComponent implements OnInit { public items : Array ; public constructor ( private database : PouchService , private zone : NgZone ) { this . items = [ ] ; } public ngOnInit ( ) { this . database . sync ( "http://localhost:4984/example" ) ; this . database . getChangeListener ( ) . subscribe ( data = > { this . zone . run ( ( ) = > { this . items . push ( data . doc ) ; } ) ; } ) ; } public insert ( ) { bootbox . prompt ( "What do you want to add?" , result = > { if ( result ) { this . database . put ( { type : "list" , title : result } , Uuid . v4 ( ) ) ; } } ) ; } }

In the above TypeScript we’re importing a few Angular 2 components, the PouchDB provider we had created as well as the UUID library for generating unique document keys.

Based on how we’ve decided to include Bootbox, it must be declared as any to prevent TypeScript errors. More information on including JavaScript browser based libraries in a TypeScript application can be found here.

Each shopping item will be stored in the items array. It is public so it can be accessed from the HTML markup. The constructor method not only initializes this array, but it also injects the PouchDB service into the page as well as NgZone. We’ll use NgZone when updating the UI from within an event listener.

Because it is bad practice to load data within the constructor method, we load it within the ngOnInit method. Data is loaded by subscribing to the event listener and synchronizing changes from the remote server.

Finally we can use Bootbox within our insert method to prompt for data and save it to the database. If syncing, the data will be saved and then replicated to Couchbase.

The HTML UI behind this logic can be found in the src/app/app.component.html file:

<div> <nav class="navbar navbar-default navbar-fixed-top" style="background-color: #CC2A2E;"> <div class="navbar-header"> <span class="navbar-brand" style="color: #FFFFFF">Couchbase Shopping</span> </div> </nav> <ul class="list-group" style="margin-top: 52px"> <li class="list-group-item" *ngFor="let item of items">{{ item.title }}</li> </ul> <a (click)="insert()" class="btn btn-circle btn-primary" style="position: fixed; bottom: 10px; right: 10px"> <span class="glyphicon glyphicon-plus"></span> </a> </div> 1 2 3 4 5 6 7 8 9 10 11 12 13 < div > < nav class = "navbar navbar-default navbar-fixed-top" style = "background-color: #CC2A2E;" > < div class = "navbar-header" > < span class = "navbar-brand" style = "color: #FFFFFF" > Couchbase Shopping < / span > < / div > < / nav > < ul class = "list-group" style = "margin-top: 52px" > < li class = "list-group-item" * ngFor = "let item of items" > { { item . title } } < / li > < / ul > < a ( click ) = "insert()" class = "btn btn-circle btn-primary" style = "position: fixed; bottom: 10px; right: 10px" > < span class = "glyphicon glyphicon-plus" > < / span > < / a > < / div >

Using Bootstrap and custom CSS, the UI has an action bar with a list of data. The list of data is populated from the items array and is added via a floating button. The floating button has the following CSS which can be added to the project’s src/app/app.component.css file:

/* Taken from http://bootsnipp.com/snippets/8deZ */ .btn-circle { width: 35px; height: 35px; text-align: center; padding: 2px 0; font-size: 20px; line-height: 1.65; border-radius: 30px; } 1 2 3 4 5 6 7 8 9 10 /* Taken from http://bootsnipp.com/snippets/8deZ */ . btn - circle { width : 35px ; height : 35px ; text - align : center ; padding : 2px 0 ; font - size : 20px ; line - height : 1.65 ; border - radius : 30px ; }

When clicked, the button will trigger the insert method. At this point we have a fully functional Angular 2 web application that replicates with Couchbase. However, our goal is to make it into an extension for Google Chrome.

Preparing and Packaging for Google Chrome Deployment

Google Chrome requires at least two things when making a Chrome extension. You must have an extension icon and a manifest file. For quality you should have icons of numerous sizes, but for simplicity we are only going to use one.

Create a new project, outside of your Angular 2 project. Maybe call it MyChromeExtension. What you want to do is build your Angular 2 project into this new Chrome extension project. For example you would do something like this:

ng build --output-path=/path/to/MyChromeExtension/ 1 ng build -- output - path =/ path / to / MyChromeExtension /

If successful, all the packaged Angular 2 project files will end up in your MyChromeExtension directory. Within that same directory, create a manifest.json file with the following:

{ "manifest_version": 2, "name": "Couchbase Shopping", "description": "Sync with Couchbase in a Google Chrome extension", "version": "1.0", "browser_action": { "default_icon": "icon.png", "default_popup": "index.html" }, "permissions": [], "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "web_accessible_resources": [ "assets/css/*", "assets/js/*", "assets/fonts/*" ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "manifest_version" : 2 , "name" : "Couchbase Shopping" , "description" : "Sync with Couchbase in a Google Chrome extension" , "version" : "1.0" , "browser_action" : { "default_icon" : "icon.png" , "default_popup" : "index.html" } , "permissions" : [ ] , "content_security_policy" : "script-src 'self' 'unsafe-eval'; object-src 'self'" , "web_accessible_resources" : [ "assets/css/*" , "assets/js/*" , "assets/fonts/*" ] }

There are a few important parts to the above manifest file. We’re saying that the extension icon will be icon.png found at the root of the project. If you haven’t already created one, create a 128×128 icon in PNG format. The page to be launched when clicking the icon in your Chrome toolbar is the index.html file which should have been generated when building your Angular 2 project.

By default, we cannot use files unless we specify them. By defining a list of web_accessible_resources we are saying that our extension is allowed to use the JavaScript, CSS, and fonts that Bootstrap left us.

We need to have a content_security_policy because the JavaScript in our project communicates with services outside our package. More specifically, the Couchbase Sync Gateway.

To install our extension into a Chrome browser, open Chrome and navigate to the extension manager.

At the top, enable developer extension and navigate to your MyChromeExtension unpacked extension directory. Loading it should place the extension in your Chrome toolbar like any other extension.

Seeing the Full Package

There was a lot to take in when it comes to this Angular 2 extension for Google Chrome. I’ve uploaded the project to GitHub in case you’d like to take it for a test drive.

Download the project and navigate into the AngularExtension directory. From that directory execute the following:

npm install ng build --output-path=../public 1 2 npm install ng build -- output - path = . . / public

Once the public directory has been created, copy the manifest and icon files into the public directory. Then you can load it into Google Chrome.

Conclusion

You just saw how to create a Google Chrome extension with Angular 2 that uses Couchbase for replicating data. This project was pretty much an Angular 2 application with Google Chrome packaging, but it demonstrated how easy it was to make these extensions.

As previously mentioned there are plenty of different ways to use Couchbase with Google Chrome extensions. You could use PouchDB and Sync Gateway, you could use HTTP requests and Sync Gateway, you could create your own web API with the Couchbase SDKs, or something else.