Native dialogs
Native dialogs are standard operating system windows: choose a file, save a file, ask a question with buttons. The user sees the familiar window of their system, and the app gets the chosen path or the clicked button. Dialogs are opened by the dialog module in the main process.
Saving to a file
Let us export all notes to a text file. In main.js:
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
ipcMain.handle('notes:export', async (event, notes) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showSaveDialog(win, {
title: 'Export notes',
defaultPath: 'notes.txt',
filters: [{ name: 'Text', extensions: ['txt'] }],
});
if (result.canceled) {
return false;
}
await fs.writeFile(result.filePath, notes.join('\n\n'), 'utf8');
return true;
});BrowserWindow.fromWebContents(event.sender) finds the window the request came from — the dialog is attached to it. The result contains canceled and filePath.
Calling from the page
// preload.js
contextBridge.exposeInMainWorld('notes', {
load: () => ipcRenderer.invoke('notes:load'),
save: (notes) => ipcRenderer.invoke('notes:save', notes),
exportText: (notes) => ipcRenderer.invoke('notes:export', notes),
});// renderer.js
document.querySelector('#export').addEventListener('click', () => {
window.notes.exportText(notes);
});Add a button with id="export" to index.html. Clicking it opens the system save window with the name notes.txt.
Opening a file
// main.js
ipcMain.handle('notes:import', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showOpenDialog(win, {
properties: ['openFile'],
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (result.canceled) {
return null;
}
return fs.readFile(result.filePaths[0], 'utf8');
});The open dialog returns a filePaths array: with properties: ['openFile', 'multiSelections'] several files can be chosen. The 'openDirectory' value lets the user pick a folder.
Asking the user
Before deleting a note, ask for confirmation:
// main.js
ipcMain.handle('notes:confirm-delete', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showMessageBox(win, {
type: 'question',
buttons: ['Delete', 'Cancel'],
defaultId: 1,
cancelId: 1,
message: 'Delete the note?',
detail: 'This cannot be undone.',
});
return result.response === 0;
});response is the index of the clicked button in buttons. defaultId picks the button for Enter, and cancelId the button used when the box is closed or Esc is pressed.
dialog methods
| Method | What it returns |
|---|---|
showOpenDialog | canceled, filePaths |
showSaveDialog | canceled, filePath |
showMessageBox | response — the button index |
showErrorBox | Nothing, it just shows an error |
Common mistake
If you do not check canceled, then when the user clicks Cancel the app tries to write a file to an empty path and fails with an error. After every file dialog check result.canceled first, and only then use the path.