Question

Suppose I'm working with an API which returns JSON data, but which has a complex or variable structure. For example, a string-valued property may be a plain literal, or may be tagged with a language:

/* first pattern */
{ "id": 1,
  "label": "a foo"
}

/* second pattern */
{ "id": 2,
  "label": [ {"value": "a foo", "lang": "en"},
             {"value": "un foo", "lang": "fr"}]
}

In my client-side code, I don't want to have view code worrying about whether a label is available in multiple-languages, and which one to pick, etc. Or I might want to hide the detailed JSON structure for other reasons. So, I might wrap the JSON value in an object with a suitable API:

/** Value object for foo instances sent from server */
var Foo = function( json ) {
  this.json = json;
};

/** Return a suitable label for this foo object */
Foo.prototype.label = function() {
  var i18n = ... ;
  if (i18n.prefLang && _.isArray(this.json.label)) // ... etc etc
};

So this is all pretty normal value-object pattern, and it's helpful because it's more decoupled from the specific JSON structure, more testable, etc. OK good.

What I currently don't see a way around is how to use one of these value objects with Backbone and Marionette. Specifically, I'd like to use a Foo object as the basis for a Backbone Model, and bind it to a Marionette ItemView. However, as far as I can see, the values in a Model are taken directly from the JSON structure - I can't see a way to recognise that the objects are functions:

var modelFoo = new Backbone.Model( foo );
> undefined
modelFoo.get( "label" ).constructor
> function Function() { [native code] }

So my question is: what is a good way to decouple the attributes of a Backbone Model from the specifics of a given JSON structure, such as a complex API value? Can value objects, models and views be made to play nice?

Edit

Let me add one more example, as I think the example above focussing on i18n issues only conveys part of my concern. Simplifying somewhat, in my domain, I have waterbodies comprising rivers, lakes and inter-tidal zones. A waterbody has associated with it one or more sampling points, and each sampling point has a latest sample. This might come back from the data API on the server as something like:

{"id": "GB12345678",
 "centre": {"lat": 1.2345, "long": "-2.3456"},
 "type": "river",
 "samplingPoints": [{"id": "sp98765",
                     "latestSample": {"date": "20130807", 
                                      "classification": "normal"}
                    }]
}

So in my view code, I could write expressions such as:

<%= waterbody.samplingPoints[0].latestSample.classification %>

or

<% if (waterbody.type === "river") { %>

but that would be horrible, and easily broken if the API format changes. Slightly better, I could abstract such manipulations out into template helper functions, but they are still hard to write tests for. What I'd like to do is have a value object class Waterbody, so that my view code can have something like:

<%= waterbody.latestClassification() %>

One of the main problems I'm finding with Marionette is the insistence on calling toJSON() on the models passed to views, but perhaps some of the computed property suggestions have a way of getting around that.

Was it helpful?

Solution

The cleanest solution IMO is to put the label accessor into the model instead of the VO:

var FooModel = Backbone.Model.extend({
    getLabel : function(){
        return this.getLocalized("label");
    },
    getLocalized : function(key){
        //return correct value from "label" array
    }
});

and let the views use FooModel#getLabel instead of FooModel#get("label")

--EDIT 1

This lib seems interesting for your use case as well: Backbone.Schema

It allows you to formally declare the type of your model's attributes, but also provides some syntax sugar for localized strings and allows you to create dynamic attributes (called 'computed properties'), composed from the values of other attributes.

--EDIT 2 (in response to the edited question)

IMO the VO returned from the server should be wrapped inside a model and this model is passed to the view. The model implements latestClassification, not the VO, this allows the view to directly call that method on the model.

OTHER TIPS

A simple approach to this (possibly to simple for your implementation) would be to override the model's parse method to return suitable attributes:

var modelFoo = Backbone.Model.extend({
    parse: function ( json ) {
        var i18n = ... ;
        if (i18n.prefLang && _.isArray(json.label)) {
            // json.label = "complex structure"
        }
        return json;
    }
});

That way only your model worries about how the data from the server is formatted without adding another layer of abstraction.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top