paxous
paxous

Reputation: 169

How to access html DOM element through electron js

I'm making a text editor with electron js, and once the user presses ctrl + s, I want the file to be saved as a txt file. The thing is though, I can't seem to find a way to directly access the div that holds the text. I've tried using a preload, but that only works once the program is run. How can I hold the element as a variable?

Heres the main javascript code:

const { app, BrowserWindow, globalShortcut } = require('electron');
const path = require('path');

// Create the main window
const createWindow = () => {

  // Adjust a few settings
  const win = new BrowserWindow({
    // What the height and width that you open up to
    width: 500,
    height: 600,

    // Minimun width and height
    minWidth: 400,
    minHeight: 400,

    icon: __dirname + '/icon.png',
    
    // Change the window title
    title: "text editor",

    webPreferences: {
      // Preload so that the javascript can access the text you write
      preload: path.join(__dirname, 'preload.js'),
    }    
  });
  
  win.loadFile('index.html');

  // Remove that ugly title bar and remove unnecessary keyboard shortcuts
  win.removeMenu();
}

// Create window on ready so that no nasty errors happen
app.whenReady().then(() => {
  createWindow();
});

app.whenReady().then(() => {

  // Global shortcut so the user has the ablitiy to exit
  globalShortcut.register('ctrl+e', () => {
    console.log("exiting...");
    app.exit();
  });

  globalShortcut.register('ctrl+s', () => {
    console.log("saving...");
  });
})


// when all windows close this app actually closes
app.on('window-all-closed', () => {
  if (process !== 'darwin') app.quit();
})

Upvotes: 1

Views: 2355

Answers (1)

midnight-coding
midnight-coding

Reputation: 3217

To get the innerText (or equivalent) of the div element in your index.html window, you will need to send a message to your render thread requesting this information. Following this, you will then need your render thread to send the innerText back to your main thread for processing (saving).

Electron's Inter-Process Communication can be confusing at times but if implemented correctly it can be simple and safe.

To learn more about the processes involved you will want to read and try and understand the following links:


Let's begin with building out your html document first. At a minimum it must include an editable <div> tag and a 'save' button.

index.html (render thread)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Test Editor</title>
        <style>
            #editor {
                width: 50vw;
                height: 50vh;
            }
        <style>
    </head>

    <body>
        <div id="content" contenteditable="true"></div>
        <input type="button" id="save" value="Save">
    </body>

    <script src="script.js"></script>
</html>

See Example: A simple but complete rich text editor for some cool ideas.


Now let's add the 'save' button and IPC message functionality.

script.js (render thread)

// IIFE (Immediately Invoked Function Expression)
(function() => {
    let content = document.getElemetById('content').innerText;

    document.getElementById('save').addEventListener('click', saveContent(content));

    window.ipcRender.receive('editor:getContent', () => { saveContent(content); });
});

function saveContent(content) {
    window.ipcRender.send('editor:saveContent', content);
}

Here is your main.js file with the following updates.

  • Add Electron's ipcMain module.
  • Add the win object to the top scope so it is noit garbage collected.
  • Listen for message(s) from the render thread (using an IFFE).
  • Add the saveContent() function (to be fully fleshed out by you).
  • Remove const from the new BrowserWindow line.
  • Return win from the createWindow() function so it can be referenced later on.
  • Update the globalShortcut ctrl+s function.

main.js (main thread)

const { app, BrowserWindow, globalShortcut, ipcMain } = require('electron');
const path = require('path');

let win = null;

// IIFE (Immediately Invoked Function Expression)
(function() => {
  ipcMain.on('editor:saveContent', (event, content) => { saveContent(content); });
})();

function saveContent(content) {
  console.log("saving...");
  // Save content...
  console.log("saved...");
}

// Create the main window
function createWindow() {

  // Adjust a few settings
  win = new BrowserWindow({
    // What the height and width that you open up to
    width: 500,
    height: 600,

    // Minimun width and height
    minWidth: 400,
    minHeight: 400,

    icon: __dirname + '/icon.png',
    
    // Change the window title
    title: "text editor",

    webPreferences: {
      // Preload so that the javascript can access the text you write
      preload: path.join(__dirname, 'preload.js'),
    }    
  });
  
  win.loadFile('index.html');

  // Remove that ugly title bar and remove unnecessary keyboard shortcuts
  win.removeMenu();

  return win;
}

// Create window on ready so that no nasty errors happen
app.on('ready', () => {
  // Create the window.
  win = createWindow();

  // Global shortcut so the user has the ability to exit
  globalShortcut.register('ctrl+e', () => {
    console.log("exiting...");
    app.exit();
  });

  // Global shortcut to save editable content.
  globalShortcut.register('ctrl+s', () => {
    console.log('ctrl+s pressed.');
    win.webContents.send('editor:getContent');
  });
})

// when all windows close this app actually closes
app.on('window-all-closed', () => {
  if (process !== 'darwin') app.quit();
})

Note that I have left the actual saving to the filesystem functionality to you. See Node.js: fs.writeFile() for more information.


Ok, the last piece of the puzzle is a working preload.js script. This is the script that grants the use of a list of whitelisted channels between the main and render threads.

In here we add the editor:saveContent and editor:getContent channel names.

preload.js (main thread)

const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;

// White-listed channels.
const ipc = {
  'render': {
    // From render to main.
    'send': [
      'editor:saveContent'
    ],
    // From main to render.
    'receive': [
      'editor:getContent'
    ],
    // From render to main and back again.
    'sendReceive': []
  }
};

contextBridge.exposeInMainWorld(
  // Allowed 'ipcRenderer' methods.
  'ipcRender', {
    // From render to main.
    send: (channel, args) => {
      let validChannels = ipc.render.send;
      if (validChannels.includes(channel)) {
        ipcRenderer.send(channel, args);
      }
    },
    // From main to render.
    receive: (channel, listener) => {
      let validChannels = ipc.render.receive;
      if (validChannels.includes(channel)) {
        // Deliberately strip event as it includes `sender`
        ipcRenderer.on(channel, (event, ...args) => listener(...args));
      }
    },
    // From render to main and back again.
    invoke: (channel, args) => {
      let validChannels = ipc.render.sendReceive;
      if (validChannels.includes(channel)) {
        return ipcRenderer.invoke(channel, args);
      }
    }
  }
);

Note that I do not perform any functions so-to-speak in the preload script. I only manage a list of channel names and the transfer of any data associated with those channel names.

Upvotes: 1

Related Questions