Friday, November 22, 2013

Formatting Dates with Moment as Backbone Computed Fields

I've gotten a little sloppy lately with my templates. Instead of formatting my dates outside of the template, I've been using Moment in the template to render different representations of the date value. The practice itself is really not the best approach. There's a good change you'll show the same value several times which requires redundant moment.format() calls. Additionally, it adds more code to a template when less is really a better choice. And finally, Moment 2.4.0 has started the process of deprecating the global scope reference in favor of defining it via an AMD library like RequireJS.

So the goal is to go from the less desirable:


   <%= moment( when ).format( 'MM/DD/YYYY' ) %>



To a value that has been already computed in the model:


   <%= when_fmt_short %>



My first thought was to just tie into the change event on a date field when initializing the model and write a function to generate all the different formats I might want to use:


var myModel = Backbone.Model.extend({

   defaults: {

      when: null,

   },

   initialize: function() {

      this.on( 'change:when', this.formatWhen );
      this.formatWhen();
   },

   formatWhen: function() {

      var val = this.get( 'when' );

      if ( val ) {
         val = moment( val );
         this.set( 'when_fmt_short', val.format('MM/DD/YYYY') );
         this.set( 'when_fmt_long', val.format('MM/DD/YYYY h:mm:ss a') );
         this.set( 'when_fmt_duration_ago', val.fromNow() );
         this.set( 'when_fmt_ellapsed_seconds', moment.duration( moment() - val ).as('seconds') + ' seconds' );
      }
   },

});



This works but the computed fields appear in the JSON payload to the server. If I want to avoid that, I need to overload toJSON to omit those fields from the output. Obviously, once I have several date fields, that's going to get tedious. Clearly, a generic solution that specifically addresses this issue either needed to be built or found. Fortunately, alexanderbeletsky/backbone-computedfields provides a nice extension to the model to describe computed field formats and dependencies. It then does the work to wire everything up and maintain that data:


var MomentModel = Backbone.Model.extend({

   defaults: {
      when: null
   },

   // Describe your computed fields
   computed: {

      when_fmt_short: {
          depends: ['when'],
          get: function ( fields ) {
              return moment( fields.when ).format( 'MM/DD/YYYY' );
          },
          toJSON: false
      },
   },
   
   // Wire everything up by attaching an instance of ComputedFields
   initialize: function () {
      this.computedFields = new Backbone.ComputedFields( this );
   },

});



The toJSON option determines whether the computed value will be included in the JSON payload. This solved the problem with sending it to the server. However, now the value would not be available when rendering the template in my view. While I could pass the model to the template and use the model.get() function everywhere, its not the option I prefer to use. ComputedFields provides a solution to avoid this situation by calling toJSON with an options hash that has the key computedFields set to true:


TestView = Backbone.View.extend({

   ...

   render: function() {

      // Since the default is to exclude the computed fields from the JSON payload,
      // make sure to pass an option to force them into the object for rendering in
      // the view:

      this.$el.html( this.template( this.model.toJSON({ computedFields: true }) ) );

      return this;
   },
   
   ...

});



I've posted a simple example in my sandbox that simply creates several formats and renders them to a basic template. While the computed fields are not limited to dates, they are the primary type of field I need to format. Next in line are probably number formats like currency. By using a consistent solution like Backbone.ComputedFields, these necessary formatting steps can be made easier to maintain and reusable across multiple templates while avoiding the use of the global scope moment function.