Get and Save Data from Render Process


#1

I am creating a markdown editor with a simplistic workspace, with added productivity features such as tabs, and a few other things. So far, I am just working on getting save file and open file working. I was able to get open file working just fine, but can’t seem to get save file to work. I register a menu, with the accelerator properly calling the function, then the main process uses mainWindow.webContents.send to send a message requesting the contents of the editor, which the render process responds with. I do the same type of thing to get the path of the current file.

From testing, I have found that the function I’m using to get the editor value is functional, however, the problem seems to be returning that data. As far as I can tell, main.js continues to execute before the reply is received, meaning that the data is not properly returned. From my understanding, this is the purpose of synchronous messages. However, I cannot figure out how to properly send a synchronous message to a render process, and return a value.

Below is some of the code used in the project.

Save File Function (called by menu):

// save file
function saveFile() {
  var contents = requestEditorValue() // editor contents
  var working = requestWorkingFile() // working file (array)
  // if path is blank, new file
  if (!working) {
    // dialog
    dialog.showSaveDialog({
      title: 'Save Markdown Document',
      filters: [
        { name: 'Markdown', extensions: ['md', 'markdown']}
      ]
    }, function(filename) {
      if (!filename) return false
      writeFile(filename, contents)
      setWorkingFile(filename)
    })
  } else { // else, save in working file
    writeFile(working[0], contents)
  }
}

Some things above are definitely not as they should, as I’m still getting used to the Electron platform. I won’t go through all the other functions called by this function, as that would take too much space. Instead, getEditorValue is the main problem.

Get Editor Value Function:

// request editor value
function requestEditorValue() {
  // send request
  mainWindow.webContents.send('hello-editorValue')
  // listen for response
  ipc.on('reply-editorValue', function(event, value) {
    return value
  })
}

All of the above code is a part of main.js, the following is the listening part in renderer.js (called from index.html).

// listen for value request
ipc.on('hello-editorValue', function(event) {
  event.sender.send('reply-editorValue', input.value)
})

The HTML of this project is quite simple, and can be viewed at git.endev.xyz/octacian/writedown as well as the CSS (style.css). The repository does not contain save and open file, only the basic editor. However, the HTML is much the same other than a small spelling fix for an attribute in the tag as well as adding data-file-path and data-file-name attributes.

Thanks!


#2

After messing around a bit and testing different methods, I was able to get around my problem by combining several functions and using setTimeout. I don’t really like my method, but I guess it may be the best way. To make it work, I put requestEditorContents and requestWorkingFile directly inside the saveFile function, instead of inside separate functions. This allowed me to send for the information, then check if it had been received with a simple if statement, then delay 1/10 of a second and check again if it hadn’t. The repeating if statement:

  // check contents and time out if needed
  function checkContents() {
    // if contents blank, delay for reply
    if (!contents || !working) {
      console.log('Save File: Waiting for editor data.')
      setTimeout(function() { checkContents() }, 100)
    } else { createSave() }
  }

With that down, I modified the IPC listeners on the renderer side, to look like this:

// listen for value request
ipc.on('hello-editorValue', function(event) {
  event.sender.send('reply-editorValue', input.value)
})

// listen for working file request
ipc.on('hello-workingFile', function(event) {
  console.log('workingFile reply')
  event.sender.send('reply-workingFile', input.dataset.filePath, input.dataset.fileName)
})

Past these three thing, I had to make several modifications to the saveFile function, as well as index.html and renderer.js (called by HTML index). However, instead of cluttering this post with all the changes, check out the official WriteDown repository, specifically this commit.


#3

Is there a specific reason you’re choosing to send the data to be written from the Main Process? Because you could instead write the file from the Renderer Process and not have to worry about wrestling with IPC.


#4

I have been working through some similar design considerations. Mainly, interoperability between multiple renderer processes. The goal of which was to keep my main.js small and not involved with feature implementation. My old fall back is dependency injection but that’s not quite an option in theses scenarios, as far as I know.

However, this problem does remind me a lot of how data is commonly passed in the iOS world. Basically, you expose a property and during view creation, assign a value to the property. Unfortunately, that doesn’t quite work in electron from what I can see.

All that said, you might consider the remote module as a mechanism for transferring data (state) between processes. I created an observable data model this way and then set up listeners on the different renderer processes. If you’re curious, it was a small configuration screen and I wanted the app to update as settings were being changed.

To caffiend’s point though, you could simply save the file in the renderer process. Even with nodeIntegration set to false, you can load the the fs module for the renderer process in the preload script.

If this post wasn’t long enough for you, here is a link to a github repo you might find interesting. It’s the companion code for a book but please don’t consider this a plug. I just thought it was interesting because in it, they build a markdown editor.

Best of luck!


#5

Sorry it has taken me so long to respond.

The reason I have to do this in the main process, is because I also have an application menu, with entries such as File -> Save. On second thought, can I register these menus in the render process?


#6

Yep!

You’ll have to require('electron').remote.Menu to get it, but it’s entirely do-able from the renderer.