Build a Markdown editor with Node Webkit and Ember pt 2

This time around we're going to go about implimenting a native menu and adding in the ability to save and open files. If you missed part 1 you can find it here and the repo is here Ready ... here we go!

A lot of the code in this section is going to be Mac specific. So if you're on a different platform, firstly sorry, secondly the process really isn't much different and if you checkout out this you should be able to follow along without too much of a problem.

At the top of app.js we're going to load up the native UI library provided by Node Webkit by adding var gui = require('nw.gui');. Now we can go about building our beautiful native menu.

In index.html modify your application template to look like

<script type='text/x-handlebars' id='application'>
  {{render 'navigation'}}
  {{outlet}}
</script>

then create an empty navigation template

<script type='text/x-handlebars' id='navigation'>
</script>

Beautiful. Let's switch over to our navigation menu and get this menu made.

App.NavigationView = Ember.View.extend({
  didInsertElement: function() {
    var that = this;
    var win = gui.Window.get();
    var menubar = new gui.Menu({type: 'menubar'});
    menubar.createMacBuiltIn('EmberMarkdown');
    win.menu = menubar;
  }
});

Build it and lo and behold there is a beautiful menu bar looking straight back at you. The code above gets the current window (gui.Window.get) then creates an empty menu of type menubar. We then use a specific Mac command that gives us the edit and window navigation menus. Then finally we say that the menu for the current window will be the menubar the we just created. Awesome!

Make it do something

So we have this stunning menu bar but it doesn't really do anything so let's rectify that situation by adding in some extra functionality.
In side of our navigation view let's create a File menu. Here's the code

...
win.menu = menubar
var file = new gui.Menu();
win.menu.insert(new gui.MenuItem({label: 'File', submenu: file}), 1);

Here we create a new empty menu and assign it to file. We then create a new menu item for our windows main navigation and assign file to is as a sub menu, and finally insert it into position 1 of our main navigation. If you build the app again you'll see file up their in all of it's glory.
Finally let's add some menu items to our file menu

...
var file = new gui.Menu();
file.insert(new gui.MenuItem({
  label: 'Save As',
  click: function() {
    alert('Save As');
  }
}));

file.insert(new gui.MenuItem({
  label: 'Save',
  click: function() {
    alert('Save');
  },
  key: 's',
  modifiers: 'cmd'
}));

file.insert(new gui.MenuItem({
  label: 'Open',
  click: function() {
    alert('Open');
  },
  key: 'o',
  modifiers: 'cmd'
}));

Here we create save, save as and open items, assign them some shortcuts and finally have them alert when they are click on. If you build your app and click on any of the menu items you should see a beautiful alert message flash before your eyes.

Open it up

Quickly modify the model for your index route to look something like

model: function() {
  return {
    body: "",
    path: null
  }
}

Now let's open up real files. In our application template add in <input style="display:none;" id="fileDialog" type="file" />. Now inside of our open menu item, change the click function to be that.get('controller').send('open');.
Let's handle the open action.

App.NavigationController = Ember.Controller.extend({
  needs: ['index'],
  index: Ember.computed.alias("controllers.index"),
  actions: {
    open: function() {
      var that = this;
      var chooser = $('#fileDialog');
      chooser.change(function(e) {
        var path = $(this).val();
        if (path === '')
          return;
        that.get('index').send('openFile', path);
        $(this).val('');
      });
      chooser.trigger('click');
    }
  }
});

All we do above is click on our file dialog, grab the value of the file selected then send that over to our index controller to handle the opening of that file. We reset the value to be an empty string having opened the file so that in Chrome and Firefox it's possible to open the same file multiple times in a row.

Let's open up the file. We need to make a couple of changes to our index controller and index view to make this work. So in our index controller up the top add editor: null, and in our index view after we've assigned this.editor add in this.controller.set('editor', this.editor);
Awesome! Let's open up the file. First off, up the top of our app.js let's require nodes file system module var fs = require('fs');
Now let's modify our index controller to open us up some files.

...
actions: {
    updateEditor: function() {
      this.editor.setValue(this.get('body'));
    },

    openFile: function(path) {
      var that = this;
      fs.readFile(path, function(err, data) {
        if (err)
          alert('Sorry something went wrong :(');
        that.setProperties({'body': data.toString(), 'path': path});
        that.send('updateEditor');
        that.editor.clearSelection();
      });
    }
  }

Here we read the file from the path that we grabbed from the file chooser. If we successfully read it we set the body of our model to be the data from the file and set the path of the file as the path. We update the editor so that it reflects the new body and finally clear the editor selection since it will select all of the newly inputed text when you open the file. Build the app and you can now open files.

Saving

In your application template add in the file dialog for saving files

<input style="display:none;" id="fileSave" type="file" nwsaveas="untitled.md" />

We give the file being saved the defaul name of untitled.md but change this to your liking.

In your navigation view change your save and save as menu items to look as such

file.insert(new gui.MenuItem({
label: 'Save As',
click: function() {
  that.get('controller').send('saveAs');
}
}));

file.insert(new gui.MenuItem({
  label: 'Save',
  click: function() {
    that.get('controller').send('save');
  },
  key: 's',
  modifiers: 'cmd'
}));

Now let's write our actions to handle saving. Underneath out open action add in

},

saveAs: function() {
  this.set('index.content.path', null);
  this.send('save');
},

save: function() {
  var path;
  var pathChanged = new $.Deferred();
  var that = this;
  var chooser = $('#fileSave');
  chooser.change(function(e) {
    path = $(this).val();
    pathChanged.resolve();
  });
  if (!that.get('index.content.path')) {
    chooser.trigger('click');
  } else {
    pathChanged.resolve();
  };
  $.when(pathChanged).done(function() {
    that.get('index').send('writeFile', path);
  });
}

}

Since a save as is fundamentally a save we're going to reuse our save action for our save as. So let's walk through the save action. We create a new deferred object so that we can wait for the user to do their stuff with the file dialog before attempting to save the file. We set up our event listener on the file input to grab the path to where they want to save the file and once we have that file, we resolve the deferred object. Next comes some save logic. We check if the current file already has a path (if it does we will save it to the same path). If it does we resolve the deferred object otherwise we simulate a click on input for the file dialog. Once the deferred object has been resolved we then send the writefile action with the path of where to save the file as the argument. If we do a save as, we set the path to be null and then call save thus forcing the file dialog input to be clicked so we can specify a new path.

Finally in our index actions add

},

writeFile: function(path) {
  var that = this;
  var path = path || this.get('path');
  fs.writeFile(path, this.get('body'), function(err){
    if (err) {
      alert('Something went wrong. Sorry :(');
    } else {
      that.set('path', path);
    }
  });

Here we either get the path that was passed in as an argument or use the path associated with the current model. We then write the body to the file and if all goes well set the path of the model to be the path that we just saved to.

That's all folks

And that's it. We now have a beautiful mark down editor capable of saving and opening files. Looking for some stuff to do next? Try implimenting settings for your app, maybe integrate with some cloud storage, allow the saving of files as gists. Really go as crazy as you like. Thanks for taking the time out of your day to check this out!!