Electron, v3

Save File to the File System

Steve Kinney

Steve Kinney

Electron, v3

Check out a free preview of the full Electron, v3 course

The "Save File to the File System" Lesson is part of the full, Electron, v3 course featured in this preview video. Here's what you'd learn in this lesson:

Steve demonstrates other applications with UI nuances involving saving files. Implementing the ability to save a file and connecting to the save file button is also covered in this segment.


Transcript from the "Save File to the File System" Lesson

>> The thing that we're missing there's a little more nuance to saving a file, we save your file behaves a little bit differently, right? For instance, if you open a file and you hit save, you do not wanna be asked where to save the file, you simply wanna save the file, right?

But if it's a brand new file and you have not saved it yet, you do wanna be asked where to save it, right? There are also some little things that we come to know and love over time about a given operating system. So for instance, if I take this same file and I open it with a native Mac app.

There are some things that you will like, maybe you never noticed it. Maybe you don't use a Mac, but there are some little things that you might've noticed. One, can see the name of the file in the window. That seems good. Two, is if I hit command, I can actually see where it is and I can actually take this and have a reference to the file that I can drag around, right?

This is again, this is a native Mac app. This is not an electron app. Next off is if I modify the file, I can see very subtly that there's a little dot in here telling me that the file has been modified and this gets a little, the opacity goes down cuz it's not a saved file.

And then upon saving the file, that goes away, right? And so those are, again, the little pieces of things that we come to expect on the UX of a given application that we can and should bring. And Electron gives you the ability to do all this stuff but the nuances of your application are as such you have to kind of instruction on what this stuff means cuz there are some things that doesn't, all right?

Like if you look on the GitHub app, that's not there but what is this unsaved file and I guess they could do stuff there but they don't. In VS code they've done a different pattern but you still got that little in the upper left corner on an unsaved file change, right?

And so, what you choose to do depends on the nuances of your application, right? But generally speaking, electron gives you all the APIs to do that it just doesn't know what you wanted to do unless you tell it. So we have to go ahead and do that, cool.

So now with saving a file we should have the opportunity to slay some of those beasts of the OS integration piece, cuz like I said before, a big especially in the new security model in Electron, new is a strong word. The modern security model in electron, a lot of this is this inter-process communication dance.

The other part is like this kind of OS integration piece. So we're saving a file. We encounter some new ones in there as well. And so we have to kind of deal with it there as well. So let's say, now that we want to have, save file, and save file, it's really just gonna be at this point the content.

And I might regret that and need to change it but we'll start with that for now. So we'll have the ability to save a file and we'll give it the, SaveFile should be the content, which is a string. Cool. It seems right. I'm just wondering why that thing didn't work before, but we'll figure it out.

Cool. And so we've got that in place as we have the content and you'll see it invoke there as well, but we actually will just start by sending a message. We'll do IPC renderer. And the pattern here that we're doing is like IPC renderer is in closure scope. It's not available to that window object at all.

All right, the context bridge is in closure scope. The only thing that's available are these four methods which are being very specific. There's no way to like send a message from the renderer process. If we loaded a third party script or something along those lines, there's no way to send something on a channel that we are unaware of and this is not gonna get you all the way there.

We have CSP headers in here, right? Like untrusted third party scripts of regular web security things also make total sense here and these are present in our HTML file and you should have them as well. Don't trust arbitrary code loaded to call whatever, right? We've got this in place, but these are the kind of core pieces kind of give us a security posture here as well.

Cool, so we've got the save file and we do some of the very same pieces that we had before on the render on the main process. So we had the export HTML, we'll just go here and give ourselves new ones, which is we need a show Save Dialog, which is going to have take the browser window just as it did before.

You can see it up there. Now nope, lowercase browser window. Doesn't know that's the function yet so I got to help IntelliSense along a little bit there with the browser window and we know that it will take the content and maybe a file path, we'll see. Cool, so we've got that in place and now a lot of this is very similar to what we had before.

We do know that it will be a marked down file, we do know that we all the same stuff that we typed previously is true, and then we get finally to the safe file with a file path and the content. Now this is interesting. So this is where the first nuance comes that we had before, which is we might already have a file path, right?

It's just maybe the content that we need in this case. So, it could be possible that how we choose to do this might be in the opposite direction as well, right? Which is, if we don't have a file path, then we need to keep it available. Now, how do you store where the file path is?

You could store it in the browser. That feels kind of weird, right? Cuz it's not the one that needs it and then you can pass it back every time. What we might choose to do is like keep track of what the current file is in the main process.

We know when they open a file, right? And we know if it's the first time that they've saved a file, right? And that we don't know what the file path is, right? But then if they save a file, we then know what the file path is. So we can kind of handle all of this here.

Otherwise we're gonna have to do a lot of back and forth and it makes sense to store it here now. Again, with one window, it is a lot easier than with multiple windows. But the answer to that one is usually just keep some kinda data structure that keeps track of the windows and any state that they need.

You can go anywhere from just a simple JavaScript object all the way up to some kinda larger data store or a pattern like Redux isn't always tied to React. You can use it here as well. So let's keep track just as a top level variable, we'll just say we got a type, which is MarkdownFile.

And that's gonna have some amount of content. And the reason why keep track of the content is, I wanna know what the content was the last time we interacted with the file system. I need this because it will let me know whether or not the file has unsafe changes or not.

Which is very simple. If current content equals previous content, no unsaved changes. If they are different, then they're different. So we'll also keep track of the file path. And since we might not have one originally, we'll make these both optional on the type and we'll just say that let current file, Markdown file will say that content is an empty string cuz that will be what is technically in the browser.

If you do a dot value on an empty text area, it's an empty string. So we're keeping track of that for a moment. And file path will just keep as undefined. I could theoretically just not defined on this object and get the same effect, but I'm gonna be nice to myself and actually see the shape of the file.

So now we know, now we get a little more nuance down here, right? We go to save the file, it's going to be a little bit of a different API because we only need to show that safe dialog, if we need to figure out where to save the file.

If they've opened the file or if they've previously saved the file, we know where to save the file, we don't need to show them that, right? So in this case, I might just do a return filePath, right? And so now theoretically we can ask for it. I don't necessarily care about the content anymore.

So, it should be a little bit different than the export cuz it's got a lot more intricacies we always wanted to show. It was very similar to the file open dialog before which is if you say open a file, what file, right? If you say export a file, we might wanna export it over and over again.

For save, it's a different kind of UX around there, and so we need to be cognizant of that as well. And so now we've got the save file, which will probably take the content, which is a string. And then we can say something like filePath, Is, CurrentFile.filePath, or go get me one, right?

And so we'll have either one of those two kind of in place. Yeah, cool. I don't want this. I don't need that right there. Cuz we've got file path, which is one way or the other, theoretically gonna be there cuz we could theoretically get undefined from this one.

So we will have to check one more time if it's there. But ideally one way or the other we should have gotten one cuz I get, I don't think you can get totally all the way here but you could cancel it. So we'll say, if there's no file path, don't save my file, right?

Otherwise we can do the Did I get that, stop with that? IntelliSense is good most of the time. Cool, so we'll write to that file path with the content, and we should have that in place. So I deviated from the plan a little bit. So let's find out if this works.

So we know that we will have a save file, message that we send along the way, and we will seek to save the file. Let's go ahead and find out if that does what we think it's going to do. I think that I probably. You notice that the nice kind of Copilot help here is there's also this getFocusedWindow, which will go ahead and try to get the frontmost window.

It'll work. It's not necessarily what I want in this case, so I'm gonna do the same pattern that I had before. Which is to always pass in a browser window. And to be clear, this is fine. There's nothing wrong with this. There might be intricacies if I had a multi-window application.

Do I have the right window? I mostly, for my own sanity, this point care about consistency across my API so far. So then we know that the browser window is gonna be the one that sent the message. I feel like that is always gonna make me happier, so I'm gonna do that instead.

All right, so now we'll go and see if we've got a current file path, if not we'll ask for one. If there's no file, still we're done, we're not doing anything here, and we should be good to go. What is going on here? Find out why I'm getting yelled at here because BrowserWindow or null?

Yeah, if there's no browser window, we have nowhere to get the file contents from Deal with that later. I don't have a use case where I wouldn't have a browser window. So we'll just keep TypeScript happy. So ideally, this should go through and give me the effect that I'm looking for.

If not, I will have to live debug and figure it out cuz I made some decisions along the way that felt right at the time and then we'll see as I think through it, cool. So there'll be some other things that we need to do here cuz we will theoretically at that point, "save-file" needs a file path, right?

Somehow, yeah, we're getting the file path, should be good along, a lot of these ways and yeah, that should do the trick for me but as you can see, there's a lot of confidence in my voice right now. so we will find out together. All right, moment of truth.

Fun fact, if you don't turn the button on, you have an issue there as well. I want to control that based on that current file context. But let's cheat for a moment cuz that button doesn't do anything currently. So let's get that in place as well. Cool. So we'll say elements.SaveFileButton.

Get that will pass it through that should do the trick for me at this point. We don't necessarily need to await anything. And then we'll also just for now, SaveFileButton does not exist. It's save markdown, cool. Are you glad to not live trying to remember, get element by IDs?

Cool, all right, so I shouldn't need any changes at that point. Awesome. And now we'll go check that folder. We've got hello and it wrote the file. So now we can save the file, but saving it again gets me to the same issue where I save it over and over and over again, that's not necessarily what we wanted, right?

So this is where those OS integrations that we're talking about become kind of important. And we need to figure out how to tell our browser window about all of these things. So begins to feel a little bit more like a native application. So let's go ahead and take a look at that.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now