Creating a text editor in Electron: part 2 - writing files
Tags: electron, javascript, node
Published: 2018-09-09 06:30:49

---
title: Creating a text editor in Electron: part 2 - writing files
published: true
description: creating a text editor with electron
tags: electron, javascript, node 
series: Creating a text editor in Electron
---

In the [last tutorial](https://dev.to/aurelkurtula/creating-a-text-editor-in-electron-reading-files-13b8) the basic structure was created. We were able to read files from a directory, list their titles in the sidebar, and we were able to read their contents on the screen. 

In this tutorial, we are going to add more interaction. To start with let's talk about the menu. Since we haven't specified a menu of our own, Electron gives us one by default, but inside  `./main.js` we can create our own buttons and have them do what we need. Let's see an example.

```javascript
const { app, BrowserWindow, Menu } = require('electron')
...
app.on('ready', function(){
    devtools = new BrowserWindow()
    window = new BrowserWindow({ x: 0, y: 0, width:800, height:600})
    window.loadURL(path.join('file://', __dirname, 'static/index.html'))
    window.setTitle('Texty')
    Menu.setApplicationMenu(Menu.buildFromTemplate([
        {
            label: app.getName(),
            submenu: [
                {
                    label: `Hello`,
                    click: () => console.log("Hello world")
                }
            ]
        }
    ]))
    
})
```

We first require the `Menu` component from Electron. Then we use it to create the menu for the application that's about to load. Above is just an example. As per usual, the first label simply opens the submenu. So for the label, we are using the app name, and then we're creating a `Hello` button which consoles a message.

Let's expand that menu. However, since the object can be huge let's add the menu in a separate component.

```javascript
// ./main.js
const menu = require('./components/Menu')
app.on('ready', function(){
    window = new BrowserWindow({ x: 0, y: 0, width:800, height:600})
    ...
    Menu.setApplicationMenu(menu(window))
    
})
```

That's how navigation can be split. 

Let's create the `./components/Menu.js` file which would return a function.

```javascript
const {app, Menu } = require('electron')
module.exports = function(win){
    return Menu.buildFromTemplate([
        {
            label: app.getName(),
            submenu: [
                { label: `Hello`, click: () => console.log("Hello world") }
            ]
        },
        {
            label: 'Edit',
            submenu: [
                {label: 'Undo', role: 'undo'  },
                {label: 'Redo', role: 'redo'  },
                {label: 'Cut', role: 'cut'  },
                {label: 'Copy', role: 'copy'  },
                {label: 'Paste', role:'paste'  },
            ]
        },
        {
            label: 'Custom Menu', 
            submenu: [/* We'll add more actions */]
        }
        
    ])    
}
```

Electron gives us a set of [roles](https://electronjs.org/docs/api/menu-item#roles) which do the heavy lifting under the hood. Follow the link to see all the roles available.

From this point forward we are going to add all our navigation as a submenu of `Custom Menu` - to keep it interesting!

## Creating a new document

So far the state of our application is such that it reads files from the disc and displays the content. (The pitfalls on this approach are discussed at the end) 

Let's add the functionality of adding new documents. 

We start by adding a button to our navigation. So in `./components/Menu.js` add the following: 

```javascript
const { NEW_DOCUMENT_NEEDED } = require('../actions/types')
module.exports = function(window){
...
{
    label: 'Custom Menu', 
    submenu: [
        {
            label: 'New',
            accelerator: 'cmd+N',
            click: () => {
                window.webContents.send(NEW_DOCUMENT_NEEDED, 'Create new document')
            }
        }
    ]
```

That creates a `New` button on the menu, `accelerator` property is to give the button a shortcut. Then upon clicking the button, we are sending a message to the rendering part of the application!

Some tutorials I've read state that this is complicated to grasp, but think of [redux](https://dev.to/aurelkurtula/introduction-to-redux-1g3), the only way to communicate with the store is via listening and dispatching messages. That's precisely the same here.

The `./main.js` deals with the back end. It gives us access to electron's modules (like the menu, access to the webcam if wanted and all sorts).

Everything in `./static/scripts/*.js` doesn't have access to the above features. This portion of the code is only concerned with manipulating the DOM. There's even a strong case against using this part of the code for any fs operations (more on that below).

Back in `./static/scripts/index.js` we would listen for `NEW_DOCUMENT_NEEDED`.

```javascript
const { ipcRenderer } = require('electron'); 
const { NEW_DOCUMENT_NEEDED } = require(path.resolve('actions/types'))
ipcRenderer.on(NEW_DOCUMENT_NEEDED, (event , data) => {
    let form = document.getElementById('form')
        form.classList.toggle('show')
    document.getElementById('title_input').focus()
    form.addEventListener('submit', function(e){
        e.preventDefault()
        // write file here ?
    })
})
```

We listen for the `NEW_DOCUMENT_NEEDED` transmission. When we hear it, we show a form (usual CSS class toggle). 

![](https://thepracticaldev.s3.amazonaws.com/i/0jc3y1livwh81ef6y6yg.gif)

Then when the form is submitted, we need to write a new file.

For this simple application, we would use `fs.writeFile` just below `// write file here ?`. However, if this were a big project, we would not want to do any file system operations on the rendering side. If the application is huge even `./main.js` wouldn't be able to handle the operation (apparently you'd need a new window which is beyond our scope). However, mainly to explore how it might be done, we'll let the `./main.js` write to system.

```javascript
const { ipcRenderer } = require('electron'); 
const {  WRITE_NEW_FILE_NEEDED } = require(path.resolve('actions/types'))
...
form.addEventListener('submit', function(e){
    e.preventDefault()
    // write file here ?
    ipcRenderer.send(WRITE_NEW_FILE_NEEDED, {
        dir: `./data/${fileName}.md`
    })
})
```

Above we are sending an object to `WRITE_NEW_FILE_NEEDED` channel (that channel name can be anything you like)

Heading over to `./main.js` we create the file and then send a message back:

```javascript
ipcMain.on(WRITE_NEW_FILE_NEEDED, (event, {dir}) => {
    fs.writeFile(dir, `Start editing ${dir}`, function(err){
        if(err){ return console.log('error is writing new file') }
        window.webContents.send(NEW_FILE_WRITTEN, `Start editing ${dir}`)
    });
})
```
Exactly the same idea when `WRITE_NEW_FILE_NEEDED` has been transmitted, get the `dir` that's been sent through that channel, write the file on that directory and send back a message that the writing process has been completed. 

Finally, back to `./statics/scripts/index.js` 

```javascript
form.addEventListener('submit', function(e){
    e.preventDefault()
    let fileName = e.target[0].value
    ...
    ipcRenderer.on(NEW_FILE_WRITTEN, function (event, message) {
        handleNewFile(e, `./data/${fileName}.md`, message)
    });
})
```

And that's that. 

Of course, you should clone the [repository](https://github.com/aurelkurtula/Texty/tree/part2) to get the full picture. The `handleNewFile` hides merely the form, handles click event for the time the app is open. And displays the content on the page. 

```javascript
const handleNewFile = function(form, dir, content){ 
    let fileName =form.target[0].value
    form.target.classList.remove('show')
    let elChild = document.createElement('li')
    elChild.innerText = fileName
    readFileContentOnClick(dir, elChild) // read file on click
    form.target[0].value = ''
    form.target.parentNode.insertBefore(elChild,form.target.nextSibling);
    document.getElementById('content').innerHTML = content;
}
```
The way I am getting my head around the communication between ipcRenderer and ipcMain is by thinking of [the basics of redux](https://dev.to/aurelkurtula/introduction-to-redux-1g3). The way we communicate with a redux store is exactly the same. 

Here's a diagram for the code we have so far

![](https://thepracticaldev.s3.amazonaws.com/i/bsr98gyi3fg34kmangpg.png)

As you can see, this dance between the two processes is an overkill for what we're doing, but this kind of thing would have to happen in order not to block the UI. As I said, chances are even this wouldn't be enough in a bigger application. I think it's not a feature, it's a bug.

## Saving changes

Finally, for this part of the series, we need to save changes.

Following the Mac pattern, I want a visual indication the file needs saving and for that indication to be removed after the file is saved. Starting in `./static/scripts/index.js`

```
document.getElementById('content').onkeyup = e => { 
    if(!document.title.endsWith("*")){ 
        document.title += ' *' 
    }; 
    ipcRenderer.send(SAVE_NEEDED, { // alerting ./component/Menu.js
        content: e.target.innerHTML,
        fileDir
    })
}
```

`onkeyup` means that something has been typed, if that's the case add an asterisk to the title and then transmit `SAVE_NEEDED` up to the main process. It will need the information that has been typed and the file directory that is being affected. 

This time we aren't going to listen in `./main.js` but in `./components/Menu.js` (which of course is part of the same process). 

```javascript
let contentToSave = ''
ipcMain.on(SAVE_NEEDED, (event, content) => {
    contentToSave = content 
})
module.exports = function(window){
    return Menu.buildFromTemplate([
        ...
        {
            label: 'Save',
            click: () => {
                if(contentToSave != ''){
                    fs.writeFile(contentToSave.fileDir, contentToSave.content, (err) => {
                        if (err) throw err;
                        window.webContents.send(SAVED, 'File Saved')
                    });
                }
            },
            accelerator: 'cmd+S'
        }
```

On `SAVE_NEEDED` we get the content transmitted. Then every time `Save` is selected we check for that content, and if it exists, we write to file. Then, once the file is written, we sent an alert to the render section, with the message `File Saved`, where we deal with it in `./static/scripts/index.js`

```javascript
ipcRenderer.on(SAVED, (event , data) => { // when saved show notification on screen
    el = document.createElement("p");
    text = document.createTextNode(data);
    el.appendChild(text)
    el.setAttribute("id", "flash");
    document.querySelector('body').prepend(el)
    setTimeout(function() { // remove notification after 1 second
        document.querySelector('body').removeChild(el);
        document.title = document.title.slice(0,-1) // remove asterisk from title
    }, 1000);
});
```      

And the end result being:

![](https://thepracticaldev.s3.amazonaws.com/i/cpq3d0d9yt5pcz3s3aiz.gif)

That's it for today!

However, I feel I need to state the obvious. I intend to focus on the basics of Electron. Hence, as you've noticed, I did not focus at all on validation. 

Few of many things we would need to do to get this to meet minimum standards for production:

- Checking whether a file already exists.
- Dealing with unsaved files when moving between them.
- Actually convert content to markdown.
- Store content using `innerText` rather than `innerHTML` (as @simonhaisz pointed out in the last tutorial).
- And many more things which might be even more important than the above.

However, none of those is electron specific hence I chose not to spend time on writing and explaining code which doesn't contribute to learning Electron. 

There will be one more tutorial in this mini-series where we'll look at adding another window and working on user preferences. 

Meanwhile, check out the project at [github, branch: part2](https://github.com/aurelkurtula/Texty/tree/part2)

{% link https://dev.to/aurelkurtula/creating-a-text-editor-in-electron-reading-files-13b8 %}

{% link https://dev.to/aurelkurtula/creating-a-text-editor-in-electron-part-3---setting-preferences-584h %}