Skip to content

Instantly share code, notes, and snippets.

@aaronj1335
Last active December 11, 2015 04:09
Show Gist options
  • Select an option

  • Save aaronj1335/4543407 to your computer and use it in GitHub Desktop.

Select an option

Save aaronj1335/4543407 to your computer and use it in GitHub Desktop.
data binding in gloss 2.0
var Binding = Class.extend({
prop: null, // required
twoWay: false,
// XOR of one of the following
// ------------------------------
widget: null,
// ------------------------------
$el: null,
// ------------------------------
getValue: function() { },
setValue: function() { },
// ------------------------------
// these would mostly be used for get/setValue, but could be used in other
// cases as well
showError: function() { },
clearError: function() { },
// this is a list of objects that are token-to-error-message mappings, i.e.
// the sort of thing that would be in string bundles. note that you can
// have multiple, so that if the error token isn't found in the first obj
// in the list, it will proceed to the next one
strings: []
});

overview

  • a binding is just a link between a certain property on a model, and a piece of UI (FormWidget instance, <span> el, etc)

    • it can be one-way (property change updates the UI) or two-way (change event from, say, and <input> el updates the model's property)
    • the display value might actually be computed instead of displayed directly, the most common case of this would be showing a string value for some kind of token
  • it's convenient to group bindings, especially with forms, in which case a 'submit' event should call .save() on the model

  • in common cases all of the binding should happen automatically -- i'm thinking like a 'data-binding' attribute that we can add to the template. common cases include:

    • a simple form (whatever 'simple' means…)
    • a details pane, like the ones that are so ubiquitous in daft
  • we also want something really modular -- i think we've got a lot of the pieces already, but they need to be broken out, specifically:

    • a Widgetizer class that just takes a dom fragment and reruns a bunch of instantiated FormWidget's (basically breaking this into its own class)
    • a BindingGroup class that tracks the relationships between model properties and ui elements. this is a decent start.
    • an ErrorPropagator class (preferably with a name that's not stupid) that understands how to take the exception thrown by Model#validate() and display the errors on the correct MessageList instances. this is basically BoundWidgetGroup#processErrors(), but it should also handle strings a bit more intelligently
  • ultimately we just want to instantiate one view (or maybe a mixin?), but i think it should combine these pieces to make everything work

corner cases and other concerns:

  • a BindingGroup should be able to handle more than one model (though there should be a main/default model so we don't need to type as much)

  • we need to provide easy ways to extend the validation process. this should probably happen by inheriting from the model we want and overriding .validate(). we also should consider async validations (like validating custom query expressions, which involves an ajax request) -- should Model#validate() always return a deferred??? something that's just resolved by default in the simple case? or maybe instead of throwing an error it should just always default to the deferred? (UPDATE: the answer is yes, .validate() always returns said deferred)

var MyBindableClass = Class.extend({
init: function() {
var self = this, grid, filterIdBinding;
this.grid = grid = PowerGrid({$el: this.$el.find('.powergrid')});
this.grid.set('messageList',
MessageList(this.$el.find('.gird-messagelist')));
filterIdBinding = Binding({
prop: 'filter_id',
getValue: function() {
return grid.get('collection')
.where({_selected: true}).get('id');
},
setValue: function(id) {
grid.get('collection').where({id: id}).set({_selected: true});
},
showError: function(e) {
grid.get('messageList').append(e);
},
clearError: function() {
grid.get('messageList').clear();
}
});
this.bindings = BindingGroup({
$el: this.$el,
widgets: widgetize(this.$el),
additionalBindings: [filterIdBinding],
strings: strings.daft.myimplementation
});
this.on('submit', function() {
self.get('model').validate().then(function() {
return self.get('model').save();
}).then(null, function(e) {
self.bindings.set('errors', e);
});
});
}
});
# generic cross-app errors
errors:
invalid: There was an error
daft:
myimplementation:
errors:
duplicate-name: A thing with that name already exists
# other error tokens that are specific to this peice of UI
<form>
Hello, <span data-binding=name></span>
<input data-binding=nickname></input>
<select data-binding=fav-color>
<option value=red></option>
<option value=blue></option>
</select>
<div class=powergrid></div>
</form>
@aaronj1335
Copy link
Copy Markdown
Author

two more common cases that i don't believe we're handling right now:

  • when we call .set(..., {validate: true}), no change events will be fired if the validation fails. this is essential so when the user inputs an invalid value, other model observers don't choke on the invalid value. the problem is that certain validations only happen server-side. so for example, if the server checks that each item has a unique name, then we could get in a situation where .set(..., {validate: true}) succeeds, triggers a change event, and then .save() fails, but other observers of the model have updated themselves with the invalid value.

    it would be nice (necessary?) to be able to do something like .set(..., {persist: true}), which wouldn't trigger a change event until the changes had been successfully .save()'ed (persist: true would imply validate: true).

  • we'll probably commonly run into situations where we want to add field-specific client-side validations. in the above scenario, you could imagine adding a validation that just makes sure a name is not currently in any of the models that the resource manager is aware of. the optimal api for this would look something like:

    MyValidatedResource = MyResource.extend({
        _validateOne: function(prop, value) {
            if (prop === 'name') {
                var existing = _.find(MyResource.models.models, function(m) {
                    return m.get('name') === value;
                });
                if (existing) {
                    throw DuplicateNameError();
                }
            }
            return this._super.apply(this, arguments);
        }
    });

    right now that'd be really difficult since we don't have good hooks into
    validating single properties.

it seems more important to get the second one, since that will give us a good way of covering most of the situations that would optimally be handled by the first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment