Skip to main content

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:

bash
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:

bash
npm install electron --save-dev

We'll also need to install some additional packages to help with the integration:

bash
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:

javascript
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:

json
{
"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:

html
<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:

typescript
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:

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:

javascript
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:

typescript
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:

html
<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:

  1. First, build your Angular application:
bash
npm run build
  1. Then, run the Electron application:
bash
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:

json
{
"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:

bash
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:

bash
npm install marked --save

Update your Angular component:

typescript
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:

typescript
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:

  1. How to integrate Angular with Electron to build desktop applications
  2. How to set up the basic structure of an Angular-Electron application
  3. How to communicate between Angular and Electron using IPC
  4. How to access native Node.js modules from your Angular application
  5. How to package your application for distribution
  6. 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

  1. Enhance the Markdown editor to include formatting buttons (bold, italic, headers)
  2. Add a dark mode toggle to your application
  3. Implement auto-save functionality to periodically save the user's work
  4. Create a simple file explorer sidebar that shows files in the current directory
  5. 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! :)