Angular Electron Integration
Introduction
Angular is a powerful front-end framework for building web applications, but what if you want to convert your Angular application into a desktop application that runs on Windows, macOS, and Linux? This is where Electron comes in.
Electron is an open-source framework that allows you to build cross-platform desktop applications using web technologies like HTML, CSS, and JavaScript. By combining Angular with Electron, you can leverage your web development skills to create native-like desktop applications.
In this tutorial, we'll learn how to integrate Angular with Electron to create a desktop application. We'll cover everything from setting up your project to packaging and distributing your application.
Prerequisites
Before we begin, ensure you have the following installed on your system:
- Node.js (version 14.x or higher)
- npm (usually comes with Node.js)
- Angular CLI (
npm install -g @angular/cli
) - Basic knowledge of Angular and TypeScript
Setting Up Your Project
Step 1: Create a New Angular Application
First, let's create a new Angular application using the Angular CLI:
ng new angular-electron-app
cd angular-electron-app
Step 2: Add Electron to Your Project
Now, let's add Electron to our Angular project:
npm install electron --save-dev
We'll also need to install some additional packages to help with the integration:
npm install electron-builder --save-dev
Step 3: Create Electron Main File
Electron requires a main file that will be the entry point for your application. Create a new file called main.js
in the root of your project:
const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');
let win;
function createWindow() {
// Create the browser window
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
// Load the index.html of the app
const indexPath = url.format({
protocol: 'file:',
pathname: path.join(__dirname, 'dist/angular-electron-app/index.html'),
slashes: true
});
win.loadURL(indexPath);
// Open the DevTools (optional)
// win.webContents.openDevTools();
// Emitted when the window is closed
win.on('closed', () => {
win = null;
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows
app.on('ready', createWindow);
// Quit when all windows are closed
app.on('window-all-closed', () => {
// On macOS it is common for applications to stay open
// until the user explicitly quits
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app
// when the dock icon is clicked and there are no other
// windows open
if (win === null) {
createWindow();
}
});
Step 4: Update package.json
Now, we need to update our package.json
file to include Electron-specific configurations:
{
"name": "angular-electron-app",
"version": "0.0.0",
"main": "main.js",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"electron": "electron .",
"electron-build": "ng build --base-href ./ && electron ."
},
// ... other configurations
}
Creating the Angular Application
Now, let's enhance our Angular application. Open src/app/app.component.html
and replace its content with:
<div class="container">
<h1>Welcome to {{ title }}!</h1>
<div class="content">
<p>This is an Electron app built with Angular!</p>
<div class="system-info">
<h3>System Information:</h3>
<p><strong>Node Version:</strong> {{ nodeVersion }}</p>
<p><strong>Chromium Version:</strong> {{ chromiumVersion }}</p>
<p><strong>Electron Version:</strong> {{ electronVersion }}</p>
<p><strong>Platform:</strong> {{ platform }}</p>
</div>
<button (click)="openDialog()">Open File Dialog</button>
</div>
</div>
Now, update the component file src/app/app.component.ts
:
import { Component, OnInit } from '@angular/core';
// Since we're using Electron with Node integration,
// we need to declare the Electron API
declare global {
interface Window {
require: any;
}
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular Electron App';
nodeVersion = '';
chromiumVersion = '';
electronVersion = '';
platform = '';
ngOnInit() {
if (window.require) {
try {
const electron = window.require('electron');
const process = window.require('process');
this.nodeVersion = process.versions.node;
this.chromiumVersion = process.versions.chrome;
this.electronVersion = process.versions.electron;
this.platform = process.platform;
} catch (error) {
console.error('Error loading electron:', error);
}
} else {
console.log('Not running in Electron environment');
}
}
openDialog() {
if (window.require) {
try {
const { dialog } = window.require('@electron/remote');
dialog.showOpenDialog({
properties: ['openFile', 'multiSelections']
}).then(result => {
if (!result.canceled && result.filePaths.length > 0) {
alert('Selected files: ' + result.filePaths.join(', '));
}
});
} catch (error) {
console.error('Error opening dialog:', error);
alert('This feature only works in Electron environment');
}
} else {
alert('This feature only works in Electron environment');
}
}
}
Let's add some basic styling in src/app/app.component.css
:
.container {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.content {
margin-top: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.system-info {
margin: 20px 0;
padding: 15px;
background-color: #e9ecef;
border-radius: 5px;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0069d9;
}
Setting Up IPC Communication
One of the powerful features of Electron is the ability to communicate between the renderer process (your Angular app) and the main process (Electron). Let's set up Inter-Process Communication (IPC):
First, update your main.js
file:
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const url = require('url');
let win;
function createWindow() {
// Create the browser window
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
// Load the index.html of the app
const indexPath = url.format({
protocol: 'file:',
pathname: path.join(__dirname, 'dist/angular-electron-app/index.html'),
slashes: true
});
win.loadURL(indexPath);
// Open the DevTools (optional)
// win.webContents.openDevTools();
// Emitted when the window is closed
win.on('closed', () => {
win = null;
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows
app.on('ready', createWindow);
// Quit when all windows are closed
app.on('window-all-closed', () => {
// On macOS it is common for applications to stay open
// until the user explicitly quits
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app
// when the dock icon is clicked and there are no other
// windows open
if (win === null) {
createWindow();
}
});
// IPC communication
ipcMain.on('open-file-dialog', (event) => {
dialog.showOpenDialog(win, {
properties: ['openFile', 'multiSelections']
}).then((result) => {
if (!result.canceled) {
event.sender.send('selected-files', result.filePaths);
}
});
});
Now, update the Angular component to use IPC communication:
import { Component, OnInit, NgZone } from '@angular/core';
declare global {
interface Window {
require: any;
}
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular Electron App';
nodeVersion = '';
chromiumVersion = '';
electronVersion = '';
platform = '';
selectedFiles: string[] = [];
ipcRenderer: any;
constructor(private zone: NgZone) {}
ngOnInit() {
if (window.require) {
try {
const electron = window.require('electron');
const process = window.require('process');
this.ipcRenderer = electron.ipcRenderer;
this.nodeVersion = process.versions.node;
this.chromiumVersion = process.versions.chrome;
this.electronVersion = process.versions.electron;
this.platform = process.platform;
// Listen for the 'selected-files' event
this.ipcRenderer.on('selected-files', (event: any, filePaths: string[]) => {
// Use NgZone to update Angular's view
this.zone.run(() => {
this.selectedFiles = filePaths;
});
});
} catch (error) {
console.error('Error loading electron:', error);
}
} else {
console.log('Not running in Electron environment');
}
}
openDialog() {
if (this.ipcRenderer) {
this.ipcRenderer.send('open-file-dialog');
} else {
alert('This feature only works in Electron environment');
}
}
}
Update the template to display selected files:
<div class="container">
<h1>Welcome to {{ title }}!</h1>
<div class="content">
<p>This is an Electron app built with Angular!</p>
<div class="system-info">
<h3>System Information:</h3>
<p><strong>Node Version:</strong> {{ nodeVersion }}</p>
<p><strong>Chromium Version:</strong> {{ chromiumVersion }}</p>
<p><strong>Electron Version:</strong> {{ electronVersion }}</p>
<p><strong>Platform:</strong> {{ platform }}</p>
</div>
<button (click)="openDialog()">Open File Dialog</button>
<div *ngIf="selectedFiles.length > 0" class="selected-files">
<h3>Selected Files:</h3>
<ul>
<li *ngFor="let file of selectedFiles">{{ file }}</li>
</ul>
</div>
</div>
</div>
Building and Running Your Application
Now that we have set up our Angular and Electron application, let's run it:
- First, build your Angular application:
npm run build
- Then, run the Electron application:
npm run electron-build
Packaging Your Application
To package your application for distribution, we'll use electron-builder
. Update your package.json
to include build configuration:
{
"name": "angular-electron-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"electron": "electron .",
"electron-build": "ng build --base-href ./ && electron .",
"pack": "electron-builder --dir",
"dist": "ng build --base-href ./ && electron-builder"
},
"build": {
"appId": "com.yourcompany.angularelectron",
"productName": "Angular Electron App",
"directories": {
"output": "release/"
},
"files": [
"dist/**/*",
"main.js",
"package.json"
],
"mac": {
"category": "public.app-category.utilities"
},
"win": {
"target": ["nsis"]
},
"linux": {
"target": ["AppImage", "deb"]
}
},
// ... other configurations
}
To build installable packages for your application, run:
npm run dist
This will create packages for your current platform in the release
directory.
Real-world Example: A Simple Markdown Editor
Let's create a simple Markdown editor to demonstrate a practical application of Angular with Electron.
First, install the required packages:
npm install marked --save
Update your Angular component:
import { Component } from '@angular/core';
import { marked } from 'marked';
declare global {
interface Window {
require: any;
}
}
@Component({
selector: 'app-root',
template: `
<div class="editor-container">
<div class="toolbar">
<button (click)="saveFile()">Save</button>
<button (click)="openFile()">Open</button>
</div>
<div class="editor-layout">
<div class="editor-section">
<textarea [(ngModel)]="markdownText"
(ngModelChange)="updatePreview()"
placeholder="Write your markdown here..."></textarea>
</div>
<div class="preview-section" [innerHTML]="htmlContent"></div>
</div>
</div>
`,
styles: [`
.editor-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.toolbar {
padding: 10px;
background-color: #f3f3f3;
border-bottom: 1px solid #ddd;
}
.toolbar button {
margin-right: 10px;
padding: 6px 12px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
.editor-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.editor-section {
flex: 1;
border-right: 1px solid #ddd;
}
.preview-section {
flex: 1;
padding: 10px;
overflow-y: auto;
}
textarea {
width: 100%;
height: 100%;
border: none;
padding: 10px;
resize: none;
font-family: monospace;
font-size: 14px;
}
`]
})
export class AppComponent {
markdownText = '# Hello, Markdown!\n\nThis is a simple markdown editor built with Angular and Electron.';
htmlContent = '';
fs: any;
dialog: any;
currentFilePath = '';
constructor() {
if (window.require) {
try {
this.fs = window.require('fs');
this.dialog = window.require('electron').remote.dialog;
} catch (error) {
console.error('Error loading Node modules:', error);
}
}
this.updatePreview();
}
updatePreview() {
this.htmlContent = marked(this.markdownText);
}
saveFile() {
if (!this.fs || !this.dialog) {
alert('This feature only works in Electron environment');
return;
}
this.dialog.showSaveDialog({
title: 'Save Markdown File',
defaultPath: this.currentFilePath || 'untitled.md',
filters: [
{ name: 'Markdown Files', extensions: ['md'] }
]
}).then((result: any) => {
if (!result.canceled && result.filePath) {
this.fs.writeFile(result.filePath, this.markdownText, (err: any) => {
if (err) {
alert('An error occurred saving the file: ' + err.message);
return;
}
this.currentFilePath = result.filePath;
alert('File saved successfully!');
});
}
});
}
openFile() {
if (!this.fs || !this.dialog) {
alert('This feature only works in Electron environment');
return;
}
this.dialog.showOpenDialog({
title: 'Open Markdown File',
filters: [
{ name: 'Markdown Files', extensions: ['md'] }
],
properties: ['openFile']
}).then((result: any) => {
if (!result.canceled && result.filePaths.length > 0) {
this.fs.readFile(result.filePaths[0], 'utf-8', (err: any, data: string) => {
if (err) {
alert('An error occurred reading the file: ' + err.message);
return;
}
this.markdownText = data;
this.updatePreview();
this.currentFilePath = result.filePaths[0];
});
}
});
}
}
Don't forget to add FormsModule to your app.module.ts
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
This example creates a simple Markdown editor with a live preview and ability to save and open .md
files from your computer.
Summary
In this tutorial, you've learned:
- How to integrate Angular with Electron to build desktop applications
- How to set up the basic structure of an Angular-Electron application
- How to communicate between Angular and Electron using IPC
- How to access native Node.js modules from your Angular application
- How to package your application for distribution
- How to create a practical application (Markdown editor) using Angular and Electron
By combining Angular and Electron, you can leverage your web development skills to build powerful cross-platform desktop applications. The integration allows you to create applications that have both the rich UI capabilities of Angular and the desktop integration features of Electron.
Additional Resources
Exercises
- Enhance the Markdown editor to include formatting buttons (bold, italic, headers)
- Add a dark mode toggle to your application
- Implement auto-save functionality to periodically save the user's work
- Create a simple file explorer sidebar that shows files in the current directory
- Add support for exporting Markdown as HTML or PDF
By completing these exercises, you'll gain a deeper understanding of how Angular and Electron work together to create powerful desktop applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)