Skip to content

Base of a Chrome extension made with Angular, plus an advanced example.

License

Notifications You must be signed in to change notification settings

jprivet-dev/chrome-extension-angular-starter-kit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Chrome Extension & Angular (Starter Kit)

Summary

1. Presentation

1.1. Versions used

Label Version

nvm/node

16.15.1

Angular

14.0.2

1.2. Starter kit

The starter kit presents the minimal architecture and configuration required for a Chrome extension, made with Angular, to interconnect the following bricks:

And allows to modify, very basically, the border color of all websites:

apply color
πŸ“Ž
For the example, we assume that all bricks (popup, options, background, content scripts) are necessary, but this will not be the case for all Chrome extensions.

1.3. Advanced example

That example is an advanced version of the starter kit. This version allows you to change the border color by domain and to experiment with dynamic updating between the popup and the options page.

πŸ“Ž
This repository will only give general technical information and will not detail the steps to build the advanced example.

1.3.1. Case #1: The colors are applied by domain

  • The colors are applied by domain from the popup.

  • The color of a page is maintained even after the page is refreshed.

  • The list of colors is updated live in the options page.

  • Applied colors can be removed from the popup.

advanced example case 1

1.3.2. Case #2: Applied colors can be removed from the popup

advanced example case 2

1.3.3. Case #3: Applied colors can be removed from the listing on the options page

advanced example case 3

1.3.4. Case #4: Preset colors can be configured

  • Four preset colors can be configured from the options page.

  • These predefined colors are updated live in the extension popup and are selectable.

  • A button allows to reset the preset colors.

advanced example case 4

2. Installation

2.1. Step #1: Clone the project

$ git clone git@github.com:jprivet-dev/chrome-extension-angular-starter-kit.git
$ cd chrome-extension-angular-starter-kit

2.2. Starter kit

2.2.1. Step #2: Build / Watch

Generate the Chrome extension in dist/starter-kit folder :

$ cd starter-kit
$ npm run build
# or
$ npm run watch

2.2.2. Step #3: Install

In Chrome, go on chrome://extensions, turn on Developer mode, and Load unpacked (choose dist/starter-kit folder).

2.3. Advanced example

2.3.1. Step #2: Build / Watch

Generate the Chrome extension in dist/advanced-example folder :

$ cd advanced-example
$ npm run build
# or
$ npm run watch

2.3.2. Step #3: Install

In Chrome, go on chrome://extensions, turn on Developer mode, and Load unpacked (choose dist/advanced-example folder).

3. Starter kit: the main stages of construction

3.1. Part #1: Popup & Options

πŸ“Ž
Here are the main stages of construction. For more details please refer to the resources.

3.1.1. Generate a new application starter-kit

$ ng new starter-kit --routing true --style scss --skip-git true --defaults --strict
$ cd starter-kit

And remplace the content of app.component.html with the following line:

<router-outlet></router-outlet>

3.1.2. Configure ESLint & Prettier (falcutative)

3.1.3. Create the Popup module and component

Create the module:

$ ng g m popup --routing

Create the component:

$ ng g c popup

And configure the routes of the Popup module:

popup-routing.module.ts
const routes: Routes = [
  {
    path: '',
    component: PopupComponent,
  },
];

3.1.4. Create the Options module and component

Create the module:

$ ng g m options --routing

Create the component:

$ ng g c options

And configure the routes of the Options module:

options-routing.module.ts
const routes: Routes = [
  {
    path: '',
    component: OptionsComponent,
  },
];

3.1.5. Create the target guard

$ ng g g target
πŸ“Ž
Use the interface CanActivate

With this guard, the urls index.html?target=popup and index.html?target=options will point to the Popup and Options modules respectively:

target.guard.ts
@Injectable({
  providedIn: 'root',
})
export class TargetGuard implements CanActivate {
  constructor(private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    const target = route.queryParams['target'];
    if (['popup', 'options'].includes(target)) {
      document.body.classList.add(target);
      this.router.navigate([`/${target}`]);
      return false;
    }
    return true;
  }
}

3.1.6. Complete the routes of the app routing

app-routing.module.ts
const routes: Routes = [
  {
    path: 'popup',
    loadChildren: () =>
      import('./popup/popup.module').then((m) => m.PopupModule),
  },
  {
    path: 'options',
    loadChildren: () =>
      import('./options/options.module').then((m) => m.OptionsModule),
  },
  { path: '**', component: AppComponent, canActivate: [TargetGuard] },
];

3.1.7. Create the first version of the manifest

Create an empty new manifest:

$ touch src/manifest.json

And copy/past the following configuration:

manifest.json
{
  "name": "Chrome Extension & Angular (Starter Kit)",
  "description": "Base of a Chrome extension made with Angular.",
  "version": "0.0.0",
  "manifest_version": 3,
  "host_permissions": ["*://*/"],
  "action": {
    "default_popup": "index.html?target=popup"
  },
  "options_page": "index.html?target=options"
}

Add this manifest.json file in the assets Angular configuration projects.starter-kit.architect.build.options:

angular.json
"assets": ["src/favicon.ico", "src/assets", "src/manifest.json"],

Finally, disable the outputHashing. Replace :

angular.json
"outputHashing": "all",

With:

angular.json
"outputHashing": "none",

3.1.8. Build & install the version #1 of the Chrome extension

Generate the Chrome extension in dist/starter-kit folder :

$ npm run build

In Chrome, go on chrome://extensions, turn on Developer mode, and Load unpacked (choose dist/starter-kit folder).

load unpacked

The extension has been successfully installed. Because no icons were included in the manifest, a generic toolbar icon will be created for the extension.

Open the drop-down Extension Menu by clicking the puzzle piece icon, and click on the pushpin icon to the right of Chrome Extension & Angular. The extension is currently pinned to your Chrome browser:

add

Click on the icon extension and see the content of the popup. Click right on the the icon extension, choose Options, and see the content of the options page:

options popup

3.2. Part #2: Background & Content scripts

πŸ“Ž
Here are the main stages of construction. For more details please refer to the resources.

3.2.1. Create background.ts

$ echo 'console.log("background works!");' > src/background.ts

3.2.2. Create background_runtime.js

$ touch src/background_runtime.js

And copy/past the following lines:

background_runtime.js
// see https://stackoverflow.com/a/67982320
try {
  importScripts("background.js", "runtime.js");
} catch (e) {
  console.error(e);
}

3.2.3. Create content.ts

$ echo 'console.log("content works!");' > src/content.ts

3.2.4. Update tsconfig.app.json

Add the background.ts and content.ts files:

tsconfig.app.json
"files": [
  "...",
  "src/background.ts",
  "src/content.ts"
]

3.2.5. Create the compiling service worker

Install the Custom Webpack Builder

$ npm i -D @angular-builders/custom-webpack

Update the projects.app.architect.build configuration :

angular.json
"build": {
  "builder": "@angular-builders/custom-webpack:browser",
  "options": {
    "assets": [
      "...",
      "src/background_runtime.js"
    ],
    "...": "...",
    "customWebpackConfig": {
      "path": "./custom-webpack.config.ts"
    }
  },

In the root of the workspace (starter-kit), create the file custom-webpack.config.ts:

$ touch custom-webpack.config.ts

And copy/past the following lines:

custom-webpack.config.ts
import type { Configuration } from 'webpack';

module.exports = {
  entry: {
    background: 'src/background.ts',
    content: 'src/content.ts',
  },
} as Configuration;

3.2.6. Complete the manifest

Add background_runtime.js and content.js to the manifest:

manifest.json
{
  "...": "...",
  "background": {
    "service_worker": "background_runtime.js"
  },
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content.js", "runtime.js"]
    }
  ]
}

3.2.7. Build & install the version #2 of the Chrome extension

Generate the Chrome extension in dist/starter-kit folder :

$ npm run build

In Chrome, go on chrome://extensions and click on the reload button:

reload

Click on Inspect views service worker to view the background script’s console log:

service worker

You can see the message "background works!":

background works

Then go on google.com (for example), open the Chrome DevTools. You can see in the console the message "content works!":

content works

3.3. Part #3: Change the border color of the websites

πŸ“Ž
Here are the main stages of construction. For more details please refer to the resources.

3.3.1. Install a color picker

$ npm i ngx-color-picker

3.3.2. Insert a color picker in the popup

Add the ColorPickerModule to the PopupModule:

popup.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ColorPickerModule } from 'ngx-color-picker';

import { PopupRoutingModule } from './popup-routing.module';
import { PopupComponent } from './popup.component';

@NgModule({
  declarations: [PopupComponent],
  imports: [CommonModule, PopupRoutingModule, ColorPickerModule],
})
export class PopupModule {}

Add the colorPicker property in the PopupComponent:

popup.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-popup',
  templateUrl: './popup.component.html',
  styleUrls: ['./popup.component.scss'],
})
export class PopupComponent {
  colorPicker: string = '';
}
πŸ’‘
We remove the unnecessary constructor() and ngOnInit()

Remove all in the template and add the color picker:

popup.component.html
<span
  [style.color]="colorPicker"
  [cpToggle]="true"
  [cpDialogDisplay]="'inline'"
  [cpPositionRelativeToArrow]="true"
  [(colorPicker)]="colorPicker"
  [cpOKButtonText]="'Apply the color'"
  [cpOKButton]="true"
>
</span>

3.3.3. Generate the Chrome extension & Test the popup

Generate the Chrome extension in dist/starter-kit folder :

$ npm run build
πŸ’‘
In this case, it will not be necessary to reload the extension in chrome://extensions.

Click on the icon extension - The color picker is displayed in the popup that opens:

popup color picker
πŸ“Ž
At this stage, no colour is applied to the site.

3.3.4. Install @types/chrome

Install the Chrome types as shown in the documentation (https://www.npmjs.com/package/@types/chrome):

$ npm install --save @types/chrome

And add chrome to the types in the TS configuration :

tsconfig.app.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": ["chrome"]
  },
  "...": "..."
}

After that, the code editor took the chrome keyword into account in my codes.

You can have several workspaces for a single project open in your code editor (https://angular.io/guide/file-structure), and you can configure the types needed for each workspace (in the tsconfig.app.json file). In this situation, your code editor will only take the types into account in the files of the relevant and configured workspace.

3.3.5. Save the border color in the Chrome storage

Create the setBorderColor() method in the PopupComponent:

popup.component.ts
// ...
export class PopupComponent {
  // ...

  setBorderColor(): void {
    chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
      chrome.storage.sync.set({ borderColor: this.colorPicker }).then(() => {
        chrome.scripting.executeScript({
          target: { tabId: tab.id as number },
          files: ['content.js', 'runtime.js'],
        });
      });
    });
  }
}

3.3.6. Apply the color from the Chrome storage in the color picker

In the PopupComponent, get the border color value from the Chrome storage:

popup.component.ts
// ...
export class PopupComponent implements OnInit {
  // ...

  ngOnInit() {
    chrome.storage.sync.get('borderColor', ({ borderColor }) => {
      this.colorPicker = borderColor ?? '';
    });
  }

  // ...
}

3.3.7. Apply the border color of all websites

In the content script, get the border color value from the Chrome storage:

content.ts
console.log('content works!');

chrome.storage.sync.get('borderColor', ({ borderColor }) => {
  console.log('apply borderColor', borderColor);
  document.body.style.border = borderColor ? `5px solid ${borderColor}` : '';
});

3.3.8. Add permissions in the manifest

Add storage, activeTab and scripting permissions to the manifest:

manifest.json
{
  "...": "...",
  "host_permissions": ["*://*/"],
  "permissions": ["storage", "activeTab", "scripting"],
  "...": "..."
}

3.3.9. Generate the Chrome extension & Test the application of the border color

Generate the Chrome extension in dist/starter-kit folder :

$ npm run build

Go on https://www.google.com, click on the icon extension, choose a color and click on the button apply:

apply color

4. Advanced example

4.1. Container, Smart & Presentational components

This advanced example basically exploits the "Container vs Presentational components" architecture principle, from which 3 types of components can be extracted:

  • containers: top-level components of the route only.

  • smarts: components that are aware of the service layer.

  • presentationals: components that take inputs and emit events upon subscriptions.

4.2. Observable Data Services

This advanced example basically exploits the "Observable Data Services" principle.

4.3. CSS & Violation of the Content Security Policy

In this advanced example, as soon as you modify the style.css file, for example:

body {
  margin: 0;
}

You will get this error at runtime:

Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present.

Because of the following line in the generated HTML:

  <style>body{margin:0}</style><link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.css"></noscript></head>

It is because of inline scripting. Angular generates code by default that violates the Content Security Policy:

You can’t use inline scripting in your Chrome App pages. The restriction bans both <script> blocks and event handlers (<button onclick="…​">).

I used this solution angular/angular-cli#20864 (comment).

Instead of

angular.json
"optimization": true

put

angular.json
"optimization": {
  "scripts": true,
  "styles": {
    "minify": true,
    "inlineCritical": false
  },
  "fonts": true
},

5. Resources & Inspiration

6. Comments, suggestions?

Feel free to make comments/suggestions to me in the Git issues section.

7. License

"Chrome Extension & Angular (Starter Kit)" is released under the MIT License


About

Base of a Chrome extension made with Angular, plus an advanced example.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published