Skip to content
Advertisement

document.getElementById(“myFile”).value gets undefined using electron

I have a very basic html file (using electron);

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> File Uploader </title>
    <link rel="stylesheet" href="style.css">
    <script defer src="render.js"></script>
</head>
<body>
    
    <h1>Drive File Uploader</h1>
    <input type="file" id="myFile" name="myFile">
    <button onclick="FileUploadPing()">Upload your file</button>

</body>
</html>

and an event listener named render.js;

const ipcRenderer = require("electron").ipcRenderer;

const FileUploadPing = () => {
  var input = document.getElementById("myFile").value
  if (input) {
    ipcRenderer.send("FileUploadPing",inputVal);
  }else{console.log("no path value")}
};

ipcRenderer.on("FileRecievePing", (event, data) => {
  alert(data)
});

But when I click submit, document.getElementById("myFile").value returns undefined

how can I pull that value?

Advertisement

Answer

This is an interesting issue that confronts many people using Electron. One could either use (via Electron) the native OS dialog.showOpenDialog([browserWindow, ]options) dialog or the html <input type=”file”> tag.

To circumvent the need to manage the is prefixed with C:fakepath issue, it is often better to use the native approach. After all, that is what Electron is best at.

Let me show you how to quickly set up a html button that when clicked, will open the native file selector dialog, and when a path is selected, return the path to the render thread for display.

In the below code, we will be using a preload.js script configured to communicate (using IPC) between the main thread and render thread(s). Context Isolation will describe this in more detail.

First off, let’s code the main.js file which will include the creation of the native file dialog.

main.js (main thread)

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

const nodePath = require("path");

// Prevent garbage collection
let window;

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

    window.loadFile('index.html')
        .then(() => { window.show(); })

    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();
    }
});

// Open the file dialog
electronIpcMain.on('message:openDialog', (event) => {
    let options = {
        title: 'Select File',
        properties: ['openFile']
    };

    electronDialog.showOpenDialog(window, options)
        .then((result) => {
            if (result.canceled) {
                console.log('User cancelled file dialog.');
                return;
            }

            event.reply('message:dialogPath', result.filePaths[0]);
        })
        .catch((error) => { console.error(error); });
})

Now, let’s create the index.html file, which for the sake of simplicity, also includes the Javascript within the <script> tags.

NB: Instead of deferring your script in the <head>, you can include it just before the closing <html> tag. Placing it here effectively performs the same thing as defer in the <head> <script> tag.

index.html (render thread)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Drive File Uploader</title>
    </head>

    <body>
        <div>Drive File Uploader</div>
        <hr>

        <label for="path">Path: </label>
        <input type="text" id="path" name="path">
        <input type="button" id="open-dialog" name="open-dialog" value="...">

        <input type="button" id="upload" value="Upload">
    </body>

    <script>
        // Let's declare it as it is used more than once
        let filePath = document.getElementById('path');

        // Event listeners
        document.getElementById('open-dialog').addEventListener('click', () => { window.ipcRender.send('message:openDialog'); });
        document.getElementById('upload').addEventListener('click', () => { console.log(filePath.value); });

        // IPC message from the main thread
        window.ipcRender.receive('message:dialogPath', (path) => { filePath.value = path; })
    </script>
</html>

Finally, let’s add the preload.js script to allow the main thread and render thread(s) to safely communicate with each other.

Note: This is where we define our whitelisted 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': [
            'message:openDialog'
        ],
        // From main to render.
        'receive': [
            'message:dialogPath'
        ],
        // From render to main and back again.
        'sendReceive': []
    }
};

// Exposed protected methods in the render process.
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);
            }
        }
    }
);

Hopefully, the above outlines how simple it can be to use (via Electron) the native dialog boxes. The benefit being they have OS specific functionality and feel.

Advertisement