- 1. Presentation
- 2. Installation
- 3. Starter kit: the main stages of construction
- 3.1. Part #1: Popup & Options
- 3.1.1. Generate a new application
starter-kit
- 3.1.2. Configure ESLint & Prettier (falcutative)
- 3.1.3. Create the Popup module and component
- 3.1.4. Create the Options module and component
- 3.1.5. Create the target guard
- 3.1.6. Complete the routes of the app routing
- 3.1.7. Create the first version of the manifest
- 3.1.8. Build & install the version #1 of the Chrome extension
- 3.1.1. Generate a new application
- 3.2. Part #2: Background & Content scripts
- 3.3. Part #3: Change the border color of the websites
- 3.3.1. Install a color picker
- 3.3.2. Insert a color picker in the popup
- 3.3.3. Generate the Chrome extension & Test the popup
- 3.3.4. Install @types/chrome
- 3.3.5. Save the border color in the Chrome storage
- 3.3.6. Apply the color from the Chrome storage in the color picker
- 3.3.7. Apply the border color of all websites
- 3.3.8. Add permissions in the manifest
- 3.3.9. Generate the Chrome extension & Test the application of the border color
- 3.1. Part #1: Popup & Options
- 4. Advanced example
- 5. Resources & Inspiration
- 6. Comments, suggestions?
- 7. License
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:
π
|
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. |
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. |
-
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.
-
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.
$ git clone [email protected]:jprivet-dev/chrome-extension-angular-starter-kit.git
$ cd chrome-extension-angular-starter-kit
Generate the Chrome extension in dist/starter-kit
folder :
$ cd starter-kit
$ npm run build
# or
$ npm run watch
In Chrome, go on chrome://extensions
, turn on Developer mode, and Load unpacked (choose dist/starter-kit
folder).
Generate the Chrome extension in dist/advanced-example
folder :
$ cd advanced-example
$ npm run build
# or
$ npm run watch
In Chrome, go on chrome://extensions
, turn on Developer mode, and Load unpacked (choose dist/advanced-example
folder).
π
|
Here are the main stages of construction. For more details please refer to the resources. |
$ 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>
Create the module:
$ ng g m popup --routing
Create the component:
$ ng g c popup
And configure the routes of the Popup module:
const routes: Routes = [
{
path: '',
component: PopupComponent,
},
];
Create the module:
$ ng g m options --routing
Create the component:
$ ng g c options
And configure the routes of the Options module:
const routes: Routes = [
{
path: '',
component: OptionsComponent,
},
];
$ 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:
@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;
}
}
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] },
];
Create an empty new manifest:
$ touch src/manifest.json
And copy/past the following configuration:
{
"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
:
"assets": ["src/favicon.ico", "src/assets", "src/manifest.json"],
Finally, disable the outputHashing
. Replace :
"outputHashing": "all",
With:
"outputHashing": "none",
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).
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:
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:
π
|
Here are the main stages of construction. For more details please refer to the resources. |
$ echo 'console.log("background works!");' > src/background.ts
$ touch src/background_runtime.js
And copy/past the following lines:
// see https://stackoverflow.com/a/67982320
try {
importScripts("background.js", "runtime.js");
} catch (e) {
console.error(e);
}
Add the background.ts
and content.ts
files:
"files": [
"...",
"src/background.ts",
"src/content.ts"
]
Install the Custom Webpack Builder
$ npm i -D @angular-builders/custom-webpack
Update the projects.app.architect.build
configuration :
"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:
import type { Configuration } from 'webpack';
module.exports = {
entry: {
background: 'src/background.ts',
content: 'src/content.ts',
},
} as Configuration;
Add background_runtime.js
and content.js
to the manifest:
{
"...": "...",
"background": {
"service_worker": "background_runtime.js"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content.js", "runtime.js"]
}
]
}
Generate the Chrome extension in dist/starter-kit
folder :
$ npm run build
In Chrome, go on chrome://extensions
and click on the reload button:
Click on Inspect views service worker to view the background scriptβs console log:
You can see the message "background works!":
Then go on google.com (for example), open the Chrome DevTools. You can see in the console the message "content works!":
π
|
Here are the main stages of construction. For more details please refer to the resources. |
Add the ColorPickerModule
to the PopupModule
:
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
:
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:
<span
[style.color]="colorPicker"
[cpToggle]="true"
[cpDialogDisplay]="'inline'"
[cpPositionRelativeToArrow]="true"
[(colorPicker)]="colorPicker"
[cpOKButtonText]="'Apply the color'"
[cpOKButton]="true"
>
</span>
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:
π
|
At this stage, no colour is applied to the site. |
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 :
{
"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.
Create the setBorderColor()
method in the PopupComponent
:
// ...
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'],
});
});
});
}
}
In the PopupComponent
, get the border color value from the Chrome storage:
// ...
export class PopupComponent implements OnInit {
// ...
ngOnInit() {
chrome.storage.sync.get('borderColor', ({ borderColor }) => {
this.colorPicker = borderColor ?? '';
});
}
// ...
}
In the content script, get the border color value from the Chrome storage:
console.log('content works!');
chrome.storage.sync.get('borderColor', ({ borderColor }) => {
console.log('apply borderColor', borderColor);
document.body.style.border = borderColor ? `5px solid ${borderColor}` : '';
});
Add storage
, activeTab
and scripting
permissions to the manifest:
{
"...": "...",
"host_permissions": ["*://*/"],
"permissions": ["storage", "activeTab", "scripting"],
"...": "..."
}
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:
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.
This advanced example basically exploits the "Observable Data Services" principle.
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
"optimization": true
put
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
-
https://www.justjeb.com/post/chrome-extension-with-angular-from-zero-to-a-little-hero
-
https://medium.com/@BiigDigital/angular-et-la-configuration-webpack-1f9398313e43
-
https://developer.chrome.com/docs/extensions/mv3/getstarted/
-
https://developer.chrome.com/docs/extensions/mv3/content_scripts/
-
https://medium.com/@marcosloic/managing-state-in-angular-2-using-rxjs-b849d6bbd5a5
Feel free to make comments/suggestions to me in the Git issues section.