DailyJS

DailyJS

The JavaScript blog.


Taglmawa
Featured

lmawa nodepad express

Nodepad Resurrection

Posted on .

When I sign in to GitHub I keep noticing new watchers appear on the
Nodepad project (it's currently at 207 watchers). Although the Let's Make a Web
App
tutorials have already
noticably aged (particularly as Mongoose changed drastically in the
middle of the series), I thought I'd give the Nodepad followers
something to look at!

I updated the libraries by editing package.json:

  • Express went from 2.2.2 to 2.4.x with no major API changes
  • Mongoose has gone from 1.2.0 to 2.0.3 without much trouble
  • Jade, Stylus and connect-mongodb have also been updated. Again, no major API changes affected Nodepad

Upgrading modules is a common task when maintaining production Node
apps. Keeping tight versions in package.json can feel like
a pain at times, but it's really important for keeping production and
staging environments sane and working with other people.

Backbone

I've updated Backbone.js to
0.5.3. This version has API changes, but the only one that affected us
was a naming change from refresh to reset:

We've taken the opportunity to clarify some naming with the 0.5.0 release. Controller is now Router, and refresh is now reset. The previous saveLocation and setLocation functions have been replaced by navigate. Backbone.sync's method signature has changed to allow the passing of arbitrary options to jQuery.ajax. Be sure to opt-in to pushState support, if you want to use it.

While I was updating the client-side code I improved the 'empty state'
handling. Now when first logging in, Nodepad will behave much better.
There's no-longer a different add document view when creating the first
document, in fact it's now possible to just press 'Save' right away (an
empty document will be created, but it makes more sense than the old
version).

Summary

The main Nodepad resurrection commit was
56f7554:

  • Client-side sorting uses toLowerCase
  • Most persistent views are now on AppView
  • Nobody keeps document model references around unless they need them directly
  • Added selectedDocument to track the currently viewed document
  • Changed refresh to reset to support Backbone 0.5.0+
  • Improved new account handling by handling an empty document list correctly

And I followed that up with the library updates. See the full history
here: Nodepad commit
history
.

Hopefully this will make Nodepad a little less confusing for people just
discovering the tutorials and app!

Featured

tutorials server node lmawa nodepad npm

Node Tutorial Part 23: npm 1.0

Posted on .

Welcome to part 23 of Let's Make a Web App, a tutorial series about
building a web app with Node. This series will
walk you through the major areas you'll need to face when building your
own applications. These tutorials are tagged with
lmawa.

Click to show previous tutorials.

npm 1.0

I upgraded to npm 1.0.3 recently. It generated a list of packages that
were incompatible, I haven't bothered reinstalling them yet.

Isaac's installation instructions seem to have solidified at:

curl http://npmjs.org/install.sh | sh

... but read the npm README before doing anything!

Changes in Nodepad

It takes a while to get used to npm 1.x, but I like the changes. A big
change is the default installation path: running npm install
package
will install package in the local
./node_modules folder.

What this means for Nodepad is we no-longer need the
require('package@version') syntax.

I've updated Nodepad to work with npm 1.0: commit
52e6b1
.

npm install

From Nodepad's directory, running npm install will build
and install the dependencies to ./node_modules. That means
it's completely self-contained from everything else on your system.
Running npm g install will install all of the
dependencies in NODE_PATH/nodepad/ - again, making the
dependencies self-contained.

Using Nodepad is now fairly simple:

$ git clone git://github.com/alexyoung/nodepad.git
$ cd nodepad
$ npm install
$ mongod
$ node app.js

This is very similar to how npm bundle used to work. When
using Express apps written by other people, their documentation may
suggest running npm bundle, but npm install
should work.

Search vs. List

The npm ls command used to search available packages. It
now lists local packages:

$ npm ls -g
├─┬ express@2.3.2 
│ ├── connect@1.4.0 
│ ├── mime@1.2.1 
│ └── qs@0.1.0 
├── highlight@0.1.0 
├── jade@0.10.6 
├── markdown@0.2.1 
├── n@0.4.1 

Using npm search will search remote packages:

$ npm search nodepad
nodepad  A notepad written with Node  =alexyoung

Why the Change?

Isaac has been blogging and discussing npm 1.0 for some time on the
Node blog and npm discussion group. A major contributing
factor is Node's 0.4 changes to the module loading system. These changes
were mentioned in the Node 0.4
announcement
:

require() now has a primitive understanding of package.json. It looks for the main script. This allows require() to work on package directories directly.

And:

A specially named directory, node_modules/, is searched in the current directory for any modules. This will hopefully encourage programmers to bundle modules rather than rely on a global namespace.

It might feel awkward to have to adapt existing applications to work
with npm 1.0, but the changes should be minimal and should make module
management easier for everyone.

Featured

tutorials server node lmawa nodepad backbone.js

Node Tutorial Part 22: Backbone (Again)

Posted on .

Welcome to part 22 of Let's Make a Web App, a tutorial series about
building a web app with Node. This series will
walk you through the major areas you'll need to face when building your
own applications. These tutorials are tagged with
lmawa.

Click to show previous tutorials.

I hadn't finished rebuilding the search interface using Backbone. The
old code used simple jQuery events and Ajax to manage the search field,
form submission, and document list. The search text input management
looked like this:

$('input[name="s"]').focus(function() {
  var element = $(this);
  if (element.val() === 'Search')
    element.val('');
});

$('input[name="s"]').blur(function() {
  var element = $(this);
  if (element.val().length === 0)
    element.val('Search');
});

This translates quite easily to Backbone using a view and some events:

var SearchView = Backbone.View.extend({
  el: $('#header .search'),

  events: {
    'focus input[name="s"]': 'focus',
    'blur input[name="s"]': 'blur'
  },

  focus: function(e) {
    var element = $(e.currentTarget);
    if (element.val() === 'Search')
      element.val('');
  },

  blur: function(e) {
    var element = $(e.currentTarget);
    if (element.val().length === 0)
      element.val('Search');
  }
}

The view's main element is the search form, and the focus
and blur events are bound in a similar way to the old code.

Next I instantiate the class in AppView, which acts as a
main container view:

AppView = Backbone.View.extend({
  initialize: function() {
    this.documentList = new DocumentList();
    this.searchView = new SearchView();
  }
});

Working With DocumentList

The existing DocumentList manages the left-hand-side list
of documents, which the search code needs to work with. Using the old
code could have confused things a little bit here, because it altered
the raw HTML rather than working through Backbone.

This is the perfect place for the Show All link that appears when a
search is active:

DocumentList = Backbone.View.extend({
  el: $('#document-list'),
  Collection: Documents,

  // This supports the search system:
  events: {
    'click #show-all': 'showAll',
  },

  initialize: function() {
    _.bindAll(this, 'render', 'addDocument', 'showAll');
    this.Collection.bind('refresh', this.render);
  },

  // ...

  showAll: function(e) {
    e.preventDefault();
    this.el.html('');
    this.Collection.fetch();
    appView.searchView.reset();
  }

This code is mostly the same, but it adds the showAll
method and event binding. Using this.Collection.fetch will
reload all the documents from the server.

As an aside: I was looking at a way of making this work with a
subclassed version of the DocumentList collection that uses
/search.json instead of the usual document list URL, but I
couldn't find a way of making Backbone POST with a request
parameter.

Searching

To actually search, I've used the original jQuery Ajax call:

SearchView = Backbone.View.extend({
  el: $('#header .search'),

  events: {
    'focus input[name="s"]': 'focus',
    'blur input[name="s"]': 'blur',
    'submit': 'submit'
  },

  initialize: function(model) {
    _.bindAll(this, 'search', 'reset');
  },

  focus: function(e) {
    var element = $(e.currentTarget);
    if (element.val() === 'Search')
      element.val('');
  },

  blur: function(e) {
    var element = $(e.currentTarget);
    if (element.val().length === 0)
      element.val('Search');
  },

  submit: function(e) {
    e.preventDefault();
    this.search($('input[name="s"]').val());
  },

  reset: function() {
    this.el.find("input[name='s']").val('Search');
  },

  search: function(value) {
    $.post('/search.json', { s: value }, function(results) {
      appView.documentList.el.html('Show All');

      if (results.length === 0) {
        alert('No results found');
      } else {
        for (var i = 0; i < results.length; i++) {
          var d = new Document(results[i]);
          appView.documentList.addDocument(d);
        }
      }
    }, 'json');
  }
});

I've also added the reset method which sets the search
input to Search after pressing Show All.

Notice that DocumentList.addDocument is reused here. In a
larger application, this kind of reuse is very important and is where
Backbone starts to show its value. Another thing to note is
SearchView could easily be in its own file, allowing us to
avoid a monolithic client-side JavaScript file that becomes hard to
manage.

Conclusion

Writing good code with client-side frameworks like Backbone requires a
lot of work, but it does take away some of the drudgery when working
with data and binding it to interfaces. The search code looks cleaner
than it did before, but even though it's quite simple it would be
interesting to refactor it.

This week's commit was
2b8e083.

Featured

tutorials server node lmawa nodepad

Node Tutorial Part 21: Connection Management

Posted on .

Welcome to part 21 of Let's Make a Web App, a tutorial series about
building a web app with Node. This series will
walk you through the major areas you'll need to face when building your
own applications. These tutorials are tagged with
lmawa.

Click to show previous tutorials.

Package Updates

I've updated all the packages to the latest versions. Express is now on
2.2.2!

HTML Preview

In the Backbone.js tutorials we already added a stub to
DocumentControls called showHTML. This is
meant to show the HTML preview, which can be obtained by viewing a
document with .html appended to the URL. I added a
convenience function to the model for getting the right URL:

  Document = Backbone.Model.extend({
    Collection: Documents,

    url: function() {
      return this.urlWithFormat('json');
    },

    urlWithFormat: function(format) {
      return this.get('id') ? '/documents/' + this.get('id') + '.' + format : '/documents.json';
    },

    // ...

Then added the event 'click #html-button': 'showHTML' to
DocumentControls and updated showHTML:

showHTML: function(e) {
  var model = this.model;
  e.preventDefault();
  $.get(this.model.urlWithFormat('html'), function(data) {
    console.log($(window).height());
    $('#html-container').html(data);
    $('#html-container').dialog({
      title: model.get('title'),
      autoOpen: true,
      modal: true,
      width: $(window).width() * 0.95,
      height: $(window).height() * 0.90
    });
  });
}

I'm using dialog from jQuery UI to display the contents of
the HTML document.

Express Responses

It was possible to make some of my server-side methods hang, like this
one:

app.post('/search.:format?', loadUser, function(req, res) {
  Document.find({ user_id: req.currentUser.id, keywords: req.body.s ? req.body.s : null },
                [], { sort: ['title', 'descending'] },
                function(err, documents) {
    switch (req.params.format) {
      case 'json':
        res.send(documents.map(function(d) {
          return { title: d.title, id: d._id };
        }));
      break;
    }
  });
});

This is poor style because an unrecognised format would cause the app to
hang. You should avoid this as much as possible when building Express
apps and always send something back to the browser:

app.post('/search.:format?', loadUser, function(req, res) {
  Document.find({ user_id: req.currentUser.id, keywords: req.body.s ? req.body.s : null },
                [], { sort: ['title', 'descending'] },
                function(err, documents) {
    switch (req.params.format) {
      case 'json':
        res.send(documents.map(function(d) {
          return { title: d.title, id: d._id };
        }));
      break;

      // Here
      default:
        res.send('Format not available', 400);
      break;
    }
  });
});

TJ Holowaychuk pointed this out to me in the comments.

I'm not sure if a 400 Bad Request is the best way to respond though.
What about 406 Not Acceptable? I'm sure there's a standard convention,
but I read through part of
rfc2616 and couldn't decide.

An additional way to safeguard against hung connections is by using
middleware to disconnect them after a period of time. The
Connect-timeout middleware by Guillermo Rauch could be used to do this. Long-running
connections are still allowed with this middleware by using the
clearTimeout method which gets added to the request
objects.

It's fairly easy to use:

var connectTimeout = require('connect-timeout@0.0.1'),
    // ...

app.configure(function() {
  app.use(connectTimeout({ time: 10000 }));
  // ...

There are options for error code and time.

This week's code is commit
2fde220
.

Featured

tutorials server node lmawa nodepad

Node Tutorial Part 20: Backbone.js

Posted on .

Welcome to part 20 of Let's Make a Web App, a tutorial series about
building a web app with Node. This series will
walk you through the major areas you'll need to face when building your
own applications. These tutorials are tagged with
lmawa.

Click to show previous tutorials.

Backbone Persistence

I haven't yet hooked up our interface and models to Backbone's
persistence layer. I generally work by relying on
model.save(attributes) and model.destroy().
The save method knows when to create or update based on if
the id attribute has been set -- remember this, because
prior to this tutorial Nodepad was using _id which
confuses Backbone.

The add/remove document toolbar could use a skeleton Backbone view like
this:

ListToolBar = Backbone.View.extend({
  el: $('#left .toolbar'),

  events: {
    'click #create-document': 'add',
    'click #delete-document': 'remove'
  },

  initialize: function(model) {
    _.bindAll(this, 'add', 'remove');
    this.model = model;
  },

  add: function(e) {
    // TODO: Create a new document
  },

  remove: function(e) {
    e.preventDefault();
    if (confirm('Are you sure you want to delete that document?')) {
      this.model.destroy();
    }
  }
});

The destroy method will delete documents using a HTTP
DELETE, the same way as my previous jQuery implementation.
We actually need to instantiate ListToolBar with a model
though, so where should that happen? I decided to put it in the
DocumentRow view:

open: function() {
  $('#document-list .selected').removeClass('selected');
  $(this.el).addClass('selected');
  this.model.display();
  this.toolbar = new ListToolBar(this.model);
}

Every time a document is selected, a toolbar will be instantiated. Now
there's a relationship between the toolbar view and the current
document.

We still need to create a new document when the +
button is pressed... This is basically a case of instantiating a
document and calling save:

add: function(e) {
  e.preventDefault();
  var d = new Document({ title: 'Untitled Document', data: '' });
  d.save();

  // Add it to the collection
  Documents.add(d);

  // addDocument is a new method I've added to DocumentList which just appends
  // the right elements to the unordered list
  appView.documentList.addDocument(d);

  // Trigger an open
  d.rowView.open();
}

There's some housekeeping going on there, but notice that we basically
just call save to make Backbone do all the boring Ajax work
for us.

DocumentControls

I've also added a view called DocumentControls which
manages updating documents. By now nothing in this should really
surprise you:

DocumentControls = Backbone.View.extend({
  el: $('#controls'),

  events: {
    'click #save-button': 'save'
  },

  initialize: function(model) {
    _.bindAll(this, 'save', 'showHTML');
    this.model = model;
  },

  save: function(e) {
    this.model.set({
      title: $('input.title').val(),
      data: $('#editor').val()
    });

    this.model.save();
    this.model.rowView.render();
    e.preventDefault();
  },

  showHTML: function(e) {
    e.preventDefault();
    // TODO
  }
});

The line that reads this.model.rowView.render() is just
triggering the DocumentRow to update its contents. For
clarity, it reads like this:

render: function() {
  $(this.el).html(this.template({
    id: this.model.id,
    title: this.model.get('title')
  }));
  return this;
}

Conclusion

I hope it's now clear that working with Backbone and REST APIs can be
less work than a mess of Ajax calls and CSS selectors.

This week's code was commit
7d5cc3d
.