Skip to content

Instantly share code, notes, and snippets.

@aaronj1335
Last active December 11, 2015 04:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aaronj1335/4543407 to your computer and use it in GitHub Desktop.
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>
@omersaeed
Copy link

Here are our thoughts on this.

  • It can be one-way (property change updates the UI) or two-way (change event from, say, and el updates the model's property)
    Updating a model at run time and for partial validation would mean that it will trigger update events. Other views of that model will thus change to show the current state of the model that is being edited. Not good. This may work but only if we do binding and validations on a copy of the model and then update the collections once that copy is successfully saved. But I am not sure if this is a good way to do this.
  • 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
    Sounds like a good idea, the getValue(), and setValue() could provide a basic functionality to format content based on a definition in strings (Locating that string could be convention based as well). We could override this if needed.
  • It's convenient to group bindings, especially with forms, in which case a 'submit' event should call .save() on the model
    in MyBindableClass i would assume that we would have a smart implementation for the getValue() and setValue() functions which will be usable for most of the common cases and only need to be overridden if needed. The Bindings.js class should also have a way to tie the API field name with a user friendly label. There should be some translation here as well that replaces the field name as reported by the API using this binding.
  • 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 see some use for this when we are just displaying details, (but we can always have multiple binding groups rather than dealing with the complexity of multiple models in a group. For the scenario of Forms, We tried that with Action / Targetset, and there are some additional concerns on top of bindings. Like, sequence of save operations for the two models, updating model values when one model is saved and the other one needs the ID. Validation failures on server side and their handling in case of a multi model save.

@aaronj1335
Copy link
Author

thanks for the invaluable input @omersaeed and @mmuzamil! a couple of action items that i think need to happen before moving forward with the implementations:

  • a way to conditionally set a value on models, something like:

    model.set({foo: 'bar'}, {validate: true})
    

    such that if the validation fails the value is rolled back to the original. this might be a bit tricky to preserve the .previous() value, but it'll be necessary to prevent the invalid values from getting displayed elsewhere in the app

  • we need to figure out how we're going to do string translation in a structured way. the 'details pane' use case where we're instantiating a Binding from the $el property (i.e. not a form) is pretty much useless without this. maybe like a data-bind-string attribute that points to an object in the string bundles where the actual value could be used as the enumeration key

@aaronj1335
Copy link
Author

if we didn't need to worry about ie8 i would push for using @kriskowal's frb. unfortunately it requires Object.defineProperty

@aaronj1335
Copy link
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