Reputation: 3715
I wrote a neat little knockout.js binding for Yesod (a Haskell web framework), but I'm having a bit of trouble dealing with optionally defined values. The Haskell part of the binding is designed to serve up a fragment of javascript which makes an Ajax call to the same handler, and which receives a JSON object which is parsed by ko.mapping. There are a few hooks for customization, too.
That's all fine and dandy, but I'm having problems dealing with optional values. If I serve up a value with an optional record, the JSON emitter treats it as optional and doesn't emit it.
So, for example, the JSON response for a request for
data Foo = Foo { bar :: Maybe Int
, baz :: Int
}
serveThis :: Foo
serveThis = Foo (Nothing) 0
is { baz: "0" }
I understand why that is and that I can change it (but I'd rather not, if possible). The problem is that when I call ko.mapping.fromJS on the JSON representation of serveThis, the bar field is not turned into an observable. Okay, I get that too. And I can use the with binding to do conditional data binding. But I don't know enough JavaScript to conditionally define computed observables.
My real code looks like:
var ViewModel = function (data) {
ko.mapping.fromJS(data, {}, this);
this.notes.stdDev.percent = ko.computed( function () {
return numeral( this.notes.stdDev() ).format("0%");
}, this);
}
So, so if the standard deviation isn't defined on the Haskell-side, it won't be emitted as a JSON field, and so ko.mapping won't make it an observable. So how do I define the percentage representation conditionally?
Upvotes: 0
Views: 201
Reputation: 5095
In the instance where the stdDev
property is omitted, is it the appropriate behavior that your view model would omit the property as well? Or, does your view model need to contain the associated observable for the property regardless of the initial value?
If the former then you just need to include some simple conditional logic similar to the following:
this.notes.stdDev.percent = ko.computed(function() {
return this.notes.stdDev ? this.notes.stdDev * 100 + "%' : 'n/a';
}, this);
This will check for the existence of a stdDev
property and return the static value 'n/a'
is the property is not defined.
However, if your view needs to all users to enter a stdDev value even if the initial value was omitted, I don't recommend using this model. Instead, you might want to consider ditching ko.mapping. I was using ko.mapping extensively about a year ago but I've recently been using it less often because I've been having better results using a JS model. Here's an example of the view model architecture that's been working for me lately.
var viewModel = (function () {
return { init: init };
function init(data) {
var self = {
notes: ko.observable()
};
self.notes(new Notes(data));
ko.applyBindings(self);
return self;
}
function Notes(data) {
return {
stdDev: ko.observable(data.notes),
otherProp: ko.observable(data.otherProp),
}
}
}());
The primary thing to notice--at least in the context of the mapping discussion--is the Notes
constructor. While likely duplicating the model being returned from your service tier, this pattern allows for a reliable data structure as opposed to the mapping solution. I fought with mapping for a good 6 months before deciding that it wasn't appropriate for all situations and this explicit model definition really adds clarity and dependability to your view model. As much as mapping can simplify your plumbing code, it really puts an unnecessary dependency on the structure of your incoming data. The fact remains that your view will crash if it references a property which doesn't exist on your view model.
If you reeeeeally want to avoid duplicating models between the two tiers, there's a compromise solution in which you define the default values for the properties you are depending on and extend your incoming data model with the default values when they're missing. jQuery's extend function works well for this function--if you're already taking a dependency on jQuery. As an example:
var noteDefaults = {
stdDev: 0
};
var input = $.extend({}, defaults, data);
ko.mapping.fromJS(input, {}, this);
Hope this helps!
Upvotes: 2