Showing posts with label javascript. Show all posts
Showing posts with label javascript. Show all posts

Tuesday, 2 June 2015

A Backgrid Cell for displaying a link to a model's Url

When creating web applications that use Backbone I've found the Backgrid library useful for rapidly creating a user interface with a lot of functionality. With just a few lines of javascript you can display a table of data that allows sorting and in-row editing, and which updates a backing Backbone collection, thereby hooking directly into the infrastructure of the rest of your web page. And, like all good javascript frameworks, it:
  • has very few dependencies - just Backbone, Underscore (which Backbone requires anyway) and jQuery (which you will almost certainly be using anyway).
  • packs down nice and small - just 25KB.
  • is very extensible...
So I'd like to share a very simple extension that I've created, a Backgrid cell that will render a hyperlink to the model that it is displaying.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    /* Cell which displays a link to the model's Url. View can be extended to set:
    *   title - title of the link
    *   target - frame that the link targets, defaults to '_self'
    */
    var ModelUri = Backgrid.UriCell.extend({
        render: function () {
            this.$el.empty();
            var rawValue = this.model.get(this.column.get("name"));
            var formattedValue = this.formatter.fromRaw(rawValue, this.model);
            this.$el.append($("<a>", {
                href: this.model.url(),
                title: this.title || formattedValue,
                target: this.target || '_self'
            }).text(formattedValue));
            this.delegateEvents();
            return this;
        }
    });

When using this cell the 'name' of the column definition should point at the model attribute that contains the link text. The column definition doesn't need to contain any information about where to load the link target from as it uses model.url() for that. If you look at the source of Backgrid itself you'll see that I've changed very little from the render method of the UriCell cell. A quick commentary on what the code does:
  • On line 8 this.column.get("name") retrieves the name of the attribute that contains the link text (the 'column' object is, itself, a model).
  • Also on line 8, this.model.get(...) retrieves the value for the link text from the bound model.
  • Line 9 uses standard Backgrid functionality to transform the raw value retrieved from the model using the cell's formatter. My cell uses the default formatter that simply returns the value that is passed to it, but if you wanted to extend ModelUri further to use a different formatter then you could.
  • Line 11 sets the link target to the value of model.url().
  • Line 13 allows the target frame to be specified, defaults to '_self'.
  • Line 14 sets the link text to the formatted value.
To use the cell you would need code that looked something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 var UserCol = Backbone.Collection.extend({
  model: Backbone.Model,
  urlRoot: '/_users'
 });
 var grid = new Backgrid.Grid({
  selector: '#gridHolder',
  collectionClass: UserCol,
  comparator: 'reference',
  columns: [
   { name: "reference", editable: false, cell: ModelUri, label: "Filename" },
   { name: "processingState", editable: false, cell: 'string', label: "Status" }
  ]
 });

If you wanted the cell to render a link that would open the model url in a new tab/window then the 'target' property of the cell needs to be set to '_blank'. This can be achieved by further extending ModelUri; one way to do this is shown below:


1
2
3
4
5
6
7
8
9
 var grid = new Backgrid.Grid({
  selector: '#gridHolder',
  collectionClass: UserCol,
  comparator: 'reference',
  columns: [
   { name: "reference", editable: false, cell: ModelUri.extend({target: '_blank'}), label: "Filename" },
   { name: "processingState", editable: false, cell: 'string', label: "Status" }
  ]
 });


Hope you find this useful!

Monday, 1 June 2015

'add' and 'reset' events from Backbone Collections

In one of my current projects I'm using Backbone to lazy load and render data in a hierarchical structure (a treeview in this case). The data that I'm displayed is returned from the backend in database order, so I set the comparator property on the Backbone Collection to ensure a more friendly sort order. And this is where I encountered a slight 'gotcha' (it wouldn't be fair to call it a bug) which I thought it might be worth documenting in case it tripped anyone else up.

I've created a JS Bin to demonstrate the gotcha which I'll talk you through here:

1. Create a basic HTML page to hold everything


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="JS Bin to demonstrate the fact that Backbone collections that have a comparator to ensure a certain sort order raise 'add' events in the unsorted order when multiple models are added via the 'Collection.set' method">
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <h1>List appears below</h1>
  <ol>
    
  </ol>
</body>
</html>

2. Define a basic Collection and View


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var ExampleCollection = Backbone.Collection.extend({
      comparator: 'title',
      model: Backbone.Model
    }),
    ExampleView = Backbone.View.extend({
      tagName: 'li',
      render: function() {
        this.$el.text(this.model.get('title'));
        return this;
      }
    });

I haven't defined a Model as I'm going to use the base Backbone.Model function to create my model objects. As you can see above:

  • ExampleCollection objects will sort their models by their 'title' attribute.
  • ExampleView objects will render a list item that will contain the 'title' attribute of their model. Note that the render function is not responsible for adding the view to the DOM (this is a fairly common approach in Backbone).

3. Create an ExampleCollection object and attach event handlers 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var col = new ExampleCollection(),
  $container = $('ol');

col.on('add', function(model, collection) {
  $container.append(new ExampleView({model: model, collection: collection}).render().$el);
});
col.on('reset', function(collection) {
  collection.each(function(model) {
    $container.append(new ExampleView({model: model, collection: collection}).render().$el);
  });
});

As you can see the two event handlers attached to 'add' and 'reset' ensure between them that whenever a model is added to the collection (whether it is added by a 'set' or by a 'reset') an ExampleView object will be created and appended to the only <ol> on the page.

4. Add some models to our collection and watch what happens!


1
2
3
4
5
6
7
col.set([new Backbone.Model({
  id: 1, title: 'Zebra'
}),new Backbone.Model({
  id: 2, title: 'Adder'
}),new Backbone.Model({
  id: 3, title: 'Porcupine'
})], {sort: true});

As we are passing {sort: true} to the set method you might expect that we will see a sorted list of models on screen, but what we actually see is this:

List appears below

  1. Zebra
  2. Adder
  3. Porcupine

Changing the line that populates the collection from col.set to col.reset results in what we expected to see first time; the models are now correctly sorted:

List appears below

  1. Adder
  2. Porcupine
  3. Zebra

Why is this? The answer is that when multiple models are added to the collection in the first example (using col.set([model1,model2], {sort: true})) although the collection is sorted correctly (you can verify this by debugging if you want) the 'add' events are raised using the original ordering of the models, not the sorted ordering. When multiple models are added using a 'reset' then the 'reset' event is passed the full, sorted collection, and so we add the view to the HTML in sort order.

You could grumble about this but I think such complaints are misplaced for two reasons:

  1. If you're populating an empty collection using multiple models then 'reset' is really the correct method to use rather than 'set'.
  2. The 'set' method is extremely versatile; it can be used to merge models, add models or to both merge and add models as part of a single operation; it can be called on sorted or unsorted collections; it can be used to populate an empty collection or make updates to a collection that is already populated. When 'set' is used in some of these scenarios it would not be meaningful or a worthwhile use of CPU time to raise the 'add' events in the sort order.
So that's why I think that this behaviour is a 'gotcha' rather than a bug. Something else worth noting is that this means that when populating collections by calling 'fetch' you need to use {reset: true} in the options to ensure that the 'reset' method is called and a 'reset' event raised.

Monday, 20 April 2015

How Backbone.Model.isNew works

I'm using Backbone in one of my current projects for a rich and responsive UI. When creating a new data item client side, I sometimes need to do a server call to get the data for the new model because setting the defaults in the new model requires logic that is only found on the server.  But (importantly) the model is still "new" at this point; it hasn't been persisted to the server.  So two server round trips are required to create the object:

It would be possible to create the model on the first call to the server and then update it on the second call, but that sets up a slightly different workflow in several ways:

  1. If the user wants to abandon the creation in step (3) we need to delete the model, or inform the user that they need to do this.
  2. If operations are auditing this would be audited as a create and an update rather than simply a create.
  3. This wouldn't work at all if some values can only be set at creation time.
So I have gone with the two stage "get defaults, commit" process shown above.

This leads to a problem; when I parse the model returned from the server at step 2 as a Backbone Model I find that Model.isNew() returns false. This means that when I commit the model through a Model.save() an "update" operation occurs instead of a "create"  Why is this?  Let's look at the definition of isNew from backbone.js:


    // A model is new if it has never been saved to the server, and lacks an id.
    isNew: function() {
      return !this.has(this.idAttribute);
    },

My model has the default value for idAttribute, "id". The id of my model on the server is a .NET
Guid, so when the server model is serialised to be returned in step 2 it will be serialised as:


{id: "00000000-0000-0000-0000-000000000000", title: null, anotherAtt: "Clever server logic set this"}

As this model does have a value for "id", Model.has("id") will return true and Model.isNew() will return false.
Help is at hand though: when I define my model I can simply override the isNew() function:


var MyModel = Backbone.Model.extend({
 isNew: function() {
  return !this.has(this.idAttribute) || this.id === '00000000-0000-0000-0000-000000000000';
 }
});

Or alternatively if I'm going to use this behaviour throughout my application I can modify this behaviour on the Model prototype:


    Backbone.Model.prototype.isNew = function () {
        return typeof this.attributes.id !== 'undefined' && this.attributes.id === '00000000-0000-0000-0000-000000000000';
    };

Either way, Backbone will now correctly distinguish between models that have been persisted and those that have not.

Monday, 3 February 2014

Customising Backbone’s Sync Module

Backbone syncI’ve started using the Backbone MVC JavaScript framework recently, and have been pleasantly surprised by how easy it is to customise bits of the framework when I need some additional functionality.  by
Backbone communicates with backend web services using the Backbone.sync function; this function examine the model object being synchronised to determine whether the operation is a “create”, “read”, “update” or “delete” and then uses jQuery.ajax to perform a HTTP POST, GET, PUT or DELETE on the backend web service.  The change I want to make is to use PROPFIND and PROPPATCH instead of GET and PUT. I may go into the reasons for this change more in subsequent posts, but for now just trust me (please) that there is a reason why I want to do this.
One of the reasons that I chose Backbone was this section from the “Extending Backbone” section in the documentation:
Many JavaScript libraries are meant to be insular and self-enclosed, where you interact with them by calling their public API, but never peek inside at the guts. Backbone.js is not that kind of library.
Because it serves as a foundation for your application, you're meant to extend and enhance it in the ways you see fit
So here goes!  The Backbone.sync function is function (method, model, options) where method = “create” | “read” | “update” | “delete”.  Looking at the source of the function, the first line of the function does this:

var type = methodMap[method];

and the definition of methodMap is:

var methodMap = {
    'create': 'POST',
    'update': 'PUT',
    'patch':  'PATCH',
    'delete': 'DELETE',
    'read':   'GET'
  };


So to use PROPFIND and PROPPATCH instead of GET and PUT I should be able to simply create a different MyApp.methodMap hash (the original variable is private to the anonymous function that defines the Backbone namespace and functions), create a new MyApp.sync function which is an exact copy of Backbone.sync but referencing my new MyApp.methodMap hash and replace Backbone.sync with MyApp.sync.  Something like this:

MyApp.methodMap = {'create': 'POST', 'update': 'PROPPATCH', 'patch':'PATCH', 'delete': 'DELETE', 'read':'PROPFIND'};
MyApp.sync = function (method, model, options) {
     var type = MyApp.methodMap[method];

     // all the rest of Backbone.sync
};
Backbone.sync = MyApp.sync;


But it doesn’t work.  Monitoring the HTTP traffic using Fiddler confirms that the PROPFIND verb is being sent correctly when I call Model.fetch() but my model isn’t actually being populated with the data returned.  Looking further into the original Backbone.sync function, on about the 42nd line it decides whether to process the returned data based upon the HTTP verb that it’s sending:

// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
  params.processData = false;
}


I just have to replace the ‘GET’ with a ‘PROPFIND’ and my model objects are populated correctly.
So to recap, all I had to do to make Backbone use PROPFIND and PROPPATCH instead of GET and PUT was to:
  1. Create a new MyApp.sync function and MyApp.methodMap hash that were exact copies of the originals.
  2. Modify the new MyApp.sync function to reference MyApp.methodMap.
  3. Modify MyApp.methodMap to return PROPFIND for read and PROPPATCH for update.
  4. Modify MyApp.sync to process returned data for PROPFIND instead of GET.
You may want to make quite different changes to Backbone.sync, but I hope you are inspired by reading this to have a go at it because it isn’t difficult.

Tuesday, 21 January 2014

Get some Backbone!

TodoMVCI’ve started looking at MVC frameworks for javascript recently.  For anyone else attempting this I would recommend the excellent TodoMVC site which contains the same simple application (a TODO list) coded using many different MVC frameworks.  If you’ve got enough time on your hands, I can see it would be a great idea to look in detail through all the implementations and pick the one that has the best combination of good technical features, flexibility and ongoing development.
But I didn’t have that much time so I had to pick one Disappointed smile.
I chose Backbone because it has:
  • Many live sites.
  • Regular releases.
  • Fairly lightweight approach (describes itself as “a library not a framework”): can be used in a variety of ways rather than prescribing just one way that you should use it.  This is particularly important to me given that there is no consensus yet on how to “do” MVC in javascript, so I want a framework that is flexible enough if I change my mind half way though my project.
  •  Open architecture: you are positively encouraged to make changes to the library if you don’t like the way part of it works.  Obviously javascript’s dynamic nature makes it a lot easier to do this than I’m used to, being a .NET statically typed man by training.
That’s all for now, I will let you know about my success (or otherwise) with Backbone in future posts.