'How can Electron communicate with its Angular-Frontend?

I'm running an Angular Frontend within a frameless Electron-Window. Because the window is frameless, I need to implement the minimizing/maximizing/unmaximizing/closing-behaviour myself. I have a button for maximize and one for unmaximize and would like to hide one of them at all times, depending on the window state.

I have node-integration set to false and wonder how I can communicate from Electron to my Angular-Frontend. Then I would only need to find a way to get my app-window and emit an event, whenever it is maximized/unmaximized and then change my UI accordingly.

My communication from Angular to Electron works like this:

in Angular I have an 'electronService' which is injected in my components and calls the electron functions.

In my preload.js I expose a function from my main.js to my renderer-process like this:

const { ipcRenderer, contextBridge } = require('electron');

contextBridge.exposeInMainWorld('electron', {
    maximizeWindow: () => {
        return ipcRenderer.invoke('electron::maximize-window');
    }
});

And in my main.js I handle the incoming calls like this:

app.whenReady().then(() => {
    ipcMain.handle('electron::maximize-window', maximizeWindow);
});

function maximizeWindow(_) {
    ...
}

Is there a way to do this in the opposite direction?



Solution 1:[1]

Though I don't use Angular, the implementation of your maximize / restore functionality is quite simple once laid out, understood and implemented correctly. The use of IPC and the detection and communication of your window state will be core to its functionality.

Desired functionality:

  • When the window is maximised, show the restore button.
  • When the window is not maximised (restored), show the maximise button.

Implementation:

  1. Our render will show all four buttons: Minimise, restore, maximise and close.
  2. On render button click, send an IPC message to the main process to implement window functionality.
  3. Depending on the window state (change) in the main process, send a message to the render process to show / hide respective buttons.
  4. On window creation, send a message to the render process to show / hide respective buttons.
  5. Detect when the title bar is double-clicked to toggle render buttons between maximise and restore.

You may wonder why point 3 is needed? It exists due to points 4 and 5. Point 4 is used on window creation. No matter what settings you use to create your window (manually or pulled from a .json file), your newly created window will dynamically display the correct button. Point 5 is when you double-click your title bar in and out of maximise / restore.


Within your preload.js script we need two functions. One to indicate which render button was click (point 2) and the other to receive the state of the window (point 3).

preload.js (main process)

const { ipcRenderer, contextBridge } = require('electron');

contextBridge.exposeInMainWorld('electron', {
    buttonClicked: (button) => {
        ipcRenderer.send('buttonClicked', button);
    },
    windowState: (state) => {
        ipcRenderer.on('windowState', state);
    }
});

Within your main.js script we add functionality to tell the render process what state the window is in upon creation (point 4).

Here, we also receive the button click message from the render process (point 2) and implement functionality accordingly.

Lastly, listen for double-clicking of the title bar (point 5).

main.js (main process)

const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;

const nodePath = require('path');

let window;

function initialiseButtons() {
    if (window.isMaximized()) {
        window.webContents.send('windowState','maximised')
    } else {
        window.webContents.send('windowState','restored')
    }
}

function createWindow() {
    const window = new electronBrowserWindow({
        x: 0,
        y: 0,
        width: 800,
        height: 600,
        frame: false,
        show: false,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            preload: nodePath.join(__dirname, 'preload.js')
        }
    });

    window.loadFile('index.html')
        // .then(() => { window.maximize(); }) // Testing
        .then(() => { initialiseButtons(); })
        .then(() => { window.show(); });

    // Double-click of title bar from restore to maximise
    window.on('maximize', () => {
        initialiseButtons();
    })

    // Double-click of title bar from maximise to restore
    window.on('unmaximize', () => {
        initialiseButtons();
    })

    return window;
}

electronApp.on('ready', () => {
    window = createWindow();
});

electronApp.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        electronApp.quit();
    }
});

electronApp.on('activate', () => {
    if (electronBrowserWindow.getAllWindows().length === 0) {
        createWindow();
    }
});

// ---

electronIpcMain.on('buttonClicked', (event, buttonClicked) => {
    if (buttonClicked === 'minimise') {
        window.minimize();
        return;
    }

    if (buttonClicked === 'restore') {
        window.restore();
        window.webContents.send('windowState','restored');
        return;
    }

    if (buttonClicked === 'maximise') {
        window.maximize();
        window.webContents.send('windowState','maximised');
        return;
    }

    if (buttonClicked === 'close') {
        window.close();
    }
})

Lastly, we send the button click event(s) to the main process (point 1) and listen for the window state upon window creation (point 4).

For simplicity, i have overlooked the use of inline styling and buttons for interactivity.

index.html (render process)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Electron Test</title>
        <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
    </head>

    <body style="margin: 0; padding: 0;">

        <div style="display: flex; flex-flow: row nowrap; padding: 5px; background-color: #ccc;">
            <div style="flex: 1 0 auto">Title</div>
            <div style="flex: 0 1 auto">
                <input type="button" id="minimise" value="_">
                <input type="button" id="restore" value="&#10064;">
                <input type="button" id="maximise" value="&#9744;">
                <input type="button" id="close" value="&#9747;">
            </div>
        </div>
    </body>

    <script>
        let restoreButton = document.getElementById('restore');
        let maximiseButton = document.getElementById('maximise');

        document.getElementById('minimise').addEventListener('click', () => {
            window.electron.buttonClicked('minimise');
        });

        document.getElementById('close').addEventListener('click', () => {
            window.electron.buttonClicked('close');
        });

        restoreButton.addEventListener('click', () => {
            window.electron.buttonClicked('restore');
        });

        maximiseButton.addEventListener('click', () => {
            window.electron.buttonClicked('maximise');
        });

        window.electron.windowState((event, state) => {
            if (state === 'maximised') {
                restoreButton.style.display = 'inline-block';
                maximiseButton.style.display = 'none';
            } else {
                restoreButton.style.display = 'none';
                maximiseButton.style.display = 'inline-block';
            }
        })
    </script>
</html>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 midnight-coding