Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

structured more like angular #817

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

Expand Down
Binary file added .nx/cache/18.3.4-nx.win32-x64-msvc.node
Binary file not shown.
7 changes: 0 additions & 7 deletions LICENSE.md

This file was deleted.

230 changes: 71 additions & 159 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,183 +1,95 @@
[![Angular Logo](https://www.vectorlogo.zone/logos/angular/angular-icon.svg)](https://angular.io/) [![Electron Logo](https://www.vectorlogo.zone/logos/electronjs/electronjs-icon.svg)](https://electronjs.org/)

![Maintained][maintained-badge]
[![Make a pull request][prs-badge]][prs]
[![License][license-badge]](LICENSE.md)

[![Linux Build][linux-build-badge]][linux-build]
[![MacOS Build][macos-build-badge]][macos-build]
[![Windows Build][windows-build-badge]][windows-build]

[![Watch on GitHub][github-watch-badge]][github-watch]
[![Star on GitHub][github-star-badge]][github-star]
[![Tweet][twitter-badge]][twitter]

# Introduction

Bootstrap and package your project with Angular 17 and Electron 30 (Typescript + SASS + Hot Reload) for creating Desktop applications.

Currently runs with:

- Angular v17.3.6
- Electron v30.0.1

With this sample, you can:

- Run your app in a local development environment with Electron & Hot reload
- Run your app in a production environment
- Execute your tests with Jest and Playwright (E2E)
- Package your app into an executable file for Linux, Windows & Mac

/!\ Hot reload only pertains to the renderer process. The main electron process is not able to be hot reloaded, only restarted.

/!\ Angular CLI & Electron Builder needs Node 18.10 or later to work correctly.

## Getting Started

*Clone this repository locally:*

``` bash
git clone https://github.com/maximegris/angular-electron.git
# **Angular with Electron**
### ***Coding Electron with Angular-like Structure: Class-Based, Decorators, and Dependency Injection***
## Handling Tray Events with Decorators

Using decorators, you can efficiently manage Tray events. Here’s how you can listen to Tray events using the `@TrayListener` decorator:
### Example: Listening for `click` and `double-click` Events

```typescript
@TrayListener(TrayEventEnum.CLICK)
onTrayClick(event: TrayEventType[TrayEventEnum.CLICK]) {
if (this.window?.isVisible()) {
this.window?.hide();
} else {
this.window?.show();
}
console.log('Tray clicked');
}

@TrayListener(TrayEventEnum.DOUBLE_CLICK)
onTrayDoubleClick(event: TrayEventType[TrayEventEnum.DOUBLE_CLICK]) {
console.log('Tray double-clicked');
}
```
## Handling BrowserWindow Events with Decorators

*Install dependencies with npm (used by Electron renderer process):*
Similarly, you can use decorators to handle BrowserWindow events effectively. For instance, to manage the `close` event, use the `@WindowListener` decorator:
### Example: Listening for the `close` Event

``` bash
npm install
```typescript
@WindowListener(WindowEventEnum.CLOSED)
onWindowClose(event: WindowEventType[WindowEventEnum.CLOSED]) {
this.window = null;
console.log('Window closed');
}
```
## Handling IPC Events with Decorators

There is an issue with `yarn` and `node_modules` when the application is built by the packager. Please use `npm` as dependencies manager.

If you want to generate Angular components with Angular-cli , you **MUST** install `@angular/cli` in npm global context.
Please follow [Angular-cli documentation](https://github.com/angular/angular-cli) if you had installed a previous version of `angular-cli`.
You can also use decorators to manage IPC events effectively. For example, to handle the `UPDATE_TRAY_TEXT` event from the main process, you can use the `@IpcListener` decorator:

``` bash
npm install -g @angular/cli
```
### Listening for the `UPDATE_TRAY_TEXT` IPC Event

*Install NodeJS dependencies with npm (used by Electron main process):*
```typescript
@IpcListener(IPCChannelEnum.UPDATE_TRAY_TEXT)
onUpdateText(event: IpcMainEvent, timeLeft: IPCChannelType[IPCChannelEnum.UPDATE_TRAY_TEXT]): void {
console.log(timeLeft);
}

``` bash
cd app/
npm install
```
## Dependency Injection

Why two package.json ? This project follow [Electron Builder two package.json structure](https://www.electron.build/tutorials/two-package-structure) in order to optimize final bundle and be still able to use Angular `ng add` feature.

## To build for development

- **in a terminal window** -> npm start

Voila! You can use your Angular + Electron app in a local development environment with hot reload!

The application code is managed by `app/main.ts`. In this sample, the app runs with a simple Angular App (http://localhost:4200), and an Electron window. \
The Angular component contains an example of Electron and NodeJS native lib import. \
You can disable "Developer Tools" by commenting `win.webContents.openDevTools();` in `app/main.ts`.

## Project structure

| Folder | Description |
|--------|--------------------------------------------------|
| app | Electron main process folder (NodeJS) |
| src | Electron renderer process folder (Web / Angular) |

## How to import 3rd party libraries
### Define a Service

This sample project runs in both modes (web and electron). To make this work, **you have to import your dependencies the right way**. \
Use the `@injectable` decorator to mark your classes for dependency injection.

There are two kind of 3rd party libraries :
- NodeJS's one - Uses NodeJS core module (crypto, fs, util...)
- I suggest you add this kind of 3rd party library in `dependencies` of both `app/package.json` and `package.json (root folder)` in order to make it work in both Electron's Main process (app folder) and Electron's Renderer process (src folder).
```typescript
import { injectable } from 'inversify';

Please check `providers/electron.service.ts` to watch how conditional import of libraries has to be done when using NodeJS / 3rd party libraries in renderer context (i.e. Angular).

- Web's one (like bootstrap, material, tailwind...)
- It have to be added in `dependencies` of `package.json (root folder)`

## Add a dependency with ng-add

You may encounter some difficulties with `ng-add` because this project doesn't use the defaults `@angular-builders`. \
For example you can find [here](HOW_TO.md) how to install Angular-Material with `ng-add`.

## Browser mode

Maybe you only want to execute the application in the browser with hot reload? Just run `npm run ng:serve:web`.

## Included Commands

| Command | Description |
|--------------------------|-------------------------------------------------------------------------------------------------------|
| `npm run ng:serve` | Execute the app in the web browser (DEV mode) |
| `npm run web:build` | Build the app that can be used directly in the web browser. Your built files are in the /dist folder. |
| `npm run electron:local` | Builds your application and start electron locally |
| `npm run electron:build` | Builds your application and creates an app consumable based on your operating system |

**Your application is optimised. Only /dist folder and NodeJS dependencies are included in the final bundle.**

## You want to use a specific lib (like rxjs) in electron main thread ?

YES! You can do it! Just by importing your library in npm dependencies section of `app/package.json` with `npm install --save XXXXX`. \
It will be loaded by electron during build phase and added to your final bundle. \
Then use your library by importing it in `app/main.ts` file. Quite simple, isn't it?

## E2E Testing

E2E Test scripts can be found in `e2e` folder.

| Command | Description |
|---------------|---------------------------|
| `npm run e2e` | Execute end to end tests |

Note: To make it work behind a proxy, you can add this proxy exception in your terminal
`export {no_proxy,NO_PROXY}="127.0.0.1,localhost"`

## Debug with VsCode

[VsCode](https://code.visualstudio.com/) debug configuration is available! In order to use it, you need the extension [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome).

Then set some breakpoints in your application's source code.
@injectable()
export class FileService {
// Implementation
}
```
### Inject Dependencies into Classes

Finally from VsCode press **Ctrl+Shift+D** and select **Application Debug** and press **F5**.
Inject dependencies into your classes using the `@inject` decorator.

Please note that Hot reload is only available in Renderer process.
```typescript
@injectable()
export class MainWindow extends MainWindowBaseClass implements OnAppReady {
private readonly TRAY_ICON_PATH = '/src/assets/icons/favicon.256x256.png';

## Want to use Angular Material ? Ngx-Bootstrap ?
constructor(@inject(FileService) protected readonly fileService: FileService) {
super();
}

Please refer to [HOW_TO file](./HOW_TO.md)
// Other methods and properties
}
```
### Set Up the DI Container

## Branch & Packages version
Configure the DI container with the services you need.

- Angular 4 & Electron 1 : Branch [angular4](https://github.com/maximegris/angular-electron/tree/angular4)
- Angular 5 & Electron 1 : Branch [angular5](https://github.com/maximegris/angular-electron/tree/angular5)
- Angular 6 & Electron 3 : Branch [angular6](https://github.com/maximegris/angular-electron/tree/angular6)
- Angular 7 & Electron 3 : Branch [angular7](https://github.com/maximegris/angular-electron/tree/angular7)
- Angular 8 & Electron 7 : Branch [angular8](https://github.com/maximegris/angular-electron/tree/angular8)
- Angular 9 & Electron 7 : Branch [angular9](https://github.com/maximegris/angular-electron/tree/angular9)
- Angular 10 & Electron 9 : Branch [angular10](https://github.com/maximegris/angular-electron/tree/angular10)
- Angular 11 & Electron 12 : Branch [angular11](https://github.com/maximegris/angular-electron/tree/angular11)
- Angular 12 & Electron 16 : Branch [angular12](https://github.com/maximegris/angular-electron/tree/angular12)
- Angular 13 & Electron 18 : Branch [angular13](https://github.com/maximegris/angular-electron/tree/angular13)
- Angular 14 & Electron 21 : Branch [angular14](https://github.com/maximegris/angular-electron/tree/angular14)
- Angular 15 & Electron 24 : Branch [angular15](https://github.com/maximegris/angular-electron/tree/angular15)
- Angular 16 & Electron 25 : Branch [angular16](https://github.com/maximegris/angular-electron/tree/angular16)
- Angular 17 & Electron 30 : (main)
-
[maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen
[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license]: https://github.com/maximegris/angular-electron/blob/main/LICENSE.md
[prs-badge]: https://img.shields.io/badge/PRs-welcome-red.svg
[prs]: http://makeapullrequest.com
```typescript
const container = useProvide([FileService]);
```
### Resolve the MainWindow Class with Dependencies

[linux-build-badge]: https://github.com/maximegris/angular-electron/workflows/Linux%20Build/badge.svg
[linux-build]: https://github.com/maximegris/angular-electron/actions?query=workflow%3A%22Linux+Build%22
[macos-build-badge]: https://github.com/maximegris/angular-electron/workflows/MacOS%20Build/badge.svg
[macos-build]: https://github.com/maximegris/angular-electron/actions?query=workflow%3A%22MacOS+Build%22
[windows-build-badge]: https://github.com/maximegris/angular-electron/workflows/Windows%20Build/badge.svg
[windows-build]: https://github.com/maximegris/angular-electron/actions?query=workflow%3A%22Windows+Build%22
Resolve your main class with its dependencies from the container.

[github-watch-badge]: https://img.shields.io/github/watchers/maximegris/angular-electron.svg?style=social
[github-watch]: https://github.com/maximegris/angular-electron/watchers
[github-star-badge]: https://img.shields.io/github/stars/maximegris/angular-electron.svg?style=social
[github-star]: https://github.com/maximegris/angular-electron/stargazers
[twitter]: https://twitter.com/intent/tweet?text=Check%20out%20angular-electron!%20https://github.com/maximegris/angular-electron%20%F0%9F%91%8D
[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/maximegris/angular-electron.svg?style=social
```typescript
const mainWindowWithDependencies = container.resolve(MainWindow);
```
85 changes: 6 additions & 79 deletions app/main.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,10 @@
import {app, BrowserWindow, screen} from 'electron';
import * as path from 'path';
import * as fs from 'fs';

let win: BrowserWindow | null = null;
const args = process.argv.slice(1),
serve = args.some(val => val === '--serve');

function createWindow(): BrowserWindow {

const size = screen.getPrimaryDisplay().workAreaSize;

// Create the browser window.
win = new BrowserWindow({
x: 0,
y: 0,
width: size.width,
height: size.height,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: (serve),
contextIsolation: false,
},
});

if (serve) {
const debug = require('electron-debug');
debug();

require('electron-reloader')(module);
win.loadURL('http://localhost:4200');
} else {
// Path when running electron executable
let pathIndex = './index.html';

if (fs.existsSync(path.join(__dirname, '../dist/index.html'))) {
// Path when running electron in local folder
pathIndex = '../dist/index.html';
}

const url = new URL(path.join('file:', __dirname, pathIndex));
win.loadURL(url.href);
}

// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store window
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
});

return win;
}
import {MainWindow} from "./windows/main.window";
import {useProvide} from "./utils/hooks/provide.hook";
import {FileService} from "./utils/services/file.service";

try {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
// Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
app.on('ready', () => setTimeout(createWindow, 400));

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
// On OS X 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();
}
});

const mainWindow = useProvide([FileService]).resolve(MainWindow)
} catch (e) {
// Catch Error
// throw e;

}

23 changes: 23 additions & 0 deletions app/utils/base-classes/main-window.base-class.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions app/utils/base-classes/main-window.base-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {Tray} from "electron";
import "reflect-metadata"; // Required for InversifyJS to work properly
import {injectable} from "inversify";
import {WindowBaseClass} from "./window.base-class";

@injectable()
export abstract class MainWindowBaseClass extends WindowBaseClass {
protected tray: Tray | null = null;
}
Loading