gmac / backbone.epoxy

Declarative data binding and computed models for Backbone
http://epoxyjs.org
MIT License
615 stars 89 forks source link

EpoxyObservable instances #8

Closed jpurcell001 closed 11 years ago

jpurcell001 commented 11 years ago

Great project. So far it feels like the best balance between easy data binding and ugly templates, especially in a Backbone-centric world. We're trying it out in a project that uses Marionette heavily and it fits right in.

This is following up on #5 - the new filters seem like they'll be very useful for generic formatting functions, things that we often would have placed into templateHelpers. Have you also considered allowing EpoxyObservables to be assigned to other objects other than EpoxyModel instances? It looks close, they already appear to be proxying the underlying model.
For example: in some cases we figure out which CSS class for an element based on a combination of different model properties, or to derive a value from a combination of models on the view. A computed property on the model could work but putting this in the model definition feels wrong - it's often view specific. Using a filter doesn't quite feel right either (though maybe that's the right answer). But being able to define computed properties of the view itself (driven off the underlying set of models), and then reference them in the bindings with things like data-bind="text: $view.formattedMessage", might fill a hole.

We might try hacking this up but wanted to get your thoughts as to where the project is going.

gmac commented 11 years ago

Hey, thanks for the feedback. Glad the project is getting some use out in the field.

Re: splitting Epoxy Observables off from the Model – that's not really possible because their implementations are intrinsically tied to their parent model. Also from a conceptual standpoint, passing an EpoxyObservable object around would basically be the same as passing a single-attribute Model around – which you can already do if you really want to (although I wouldn't recommend it!). Itemized data stores are a central concept in Knockout.js, and I personally feel that's one of its weakest points.

However, I will say – your observation about computed properties of the view is very interesting. That gets back to the central ideal of a designated ViewModel, which is really just a workflow consideration at this point using existing Epoxy components. The library should probably set a patterned precedent for designating a model on the view that is only used for view attributes (computed or otherwise). Technically you can already set this up using Epoxy's raw pieces... I'd encourage experimentation and keep us posted on what workflow seems to work!

Lastly, for your current implementation – don't forget you can add multiple data sources to a view, and then run their respective attributes through filters together to create view values. As in:

bindingSources: {
    model1: new MyModel({name: "A"}),
    model2: new MyModel({name: "B"})
}

Then bind as:

data-bind="text:names(model1_name,model2_name)"

Or:

data-bind="text:names($model1, $model2)"

Feedback on the binding sources API would be greatly appreciated. I've never actually used the API for a practical scenario, so I don't know how natural it really is to use. It was very much engineered as a technical solution to an open problem.

gmac commented 11 years ago

I just committed some new code to the edge branch that adds computed properties to the view; I'd appreciate any feedback on it. It's still experimental and not formally documented, so I'll include implementation specs here...

The Epoxy.View object adds two new members: get and bindingComputeds.

view.get( attribute ) : gets a source reference or data attribute from the view's binding context. Basically: it gets the current value of any data property available to view bindings.

bindingComputeds : a hash table defining a list of read-only computed view properties to be added to the view's binding context. Computed view properties are invoked in the context of the view, allowing the computed handler to use the view's get method to access other data binding properties. Computed view properties should format and return a value, note that conditional logic warnings apply.

A view implementation looks like this:

var TestView = Backbone.Epoxy.View.extend({
    el: "#epoxy-view",

    model: new Backbone.Model({firstName:"Luke", lastName:"Skywalker"}),

    bindingComputeds: {
        fullName: function() {
            return this.get("firstName") +" "+ this.get("lastName");
        }
    }
});

When binding, computeds may be applied just like normal data properties:

data-bind="text:fullName"

Note that computed view properties are added into the binding context, and will therefore override model attribute values with the same name. Be mindful of naming collisions.

jpurcell001 commented 11 years ago

Just tried it out and it seemed to work great. Couple comments: 1) We don't extend Epoxy.View (using Marionette and our own base classes that extend it) and get is in some cases already claimed - would something like getContextProperty, while more to type, be clearer? Also makes it clear that you are working with something that, while model-like, is not a model. 2) Would be nice if bindingComputeds could be a function, similar to other Backbone declarations, e.g. _.result(self, "bindingComputeds") instead of self.bindingComputeds. This would be useful if the list of computed properties needs to be dynamically evaluated at instantiation time. 3) bindingComputeds is a mouthful - found it difficult to remember. But I haven't come up with anything better - computedViewProperties? computedProperties?

gmac commented 11 years ago

Thanks for the feedback, in RE:

1) I do whole-heartedly agree about the model-like-but-not-actually-model consideration. I've been considering that as well. Also, I'm considering the practical application of including setters in the computed view property scheme... While Epoxy's model observables were originally conceived as non-data attributes for view data, their presence within a model still doesn't make them appropriate for that purpose. I can see a real use case for maintaining bindable getters and setters exclusively on the view. However, going that route may just merit retooling the EpoxyObservable object so that –like you said– they could be registered on models OR views. For the time being, let's say that the computed view properties API remains highly experimental.

2) Ha, I've never noticed the "result" Underscore method documentation way down in the utilities section. Handy... that can replace a few lines of code around Epoxy. I'm down with the result approach, although I am curious: do you see a major use case for dynamically assembling a table of computeds? One slick thing about computeds is that they can be safely defined on prototype and shared by all instances given that they're just operational functions. The only real reason to build a computeds table for each instance would be to customize the computeds they configure for themselves (which seems like would be just as easy to make a canonical list on prototype and allow instances to use a subset of that).

3) Yep, agreed. Also, that's getting into a lot of "bindingX" properties on the view, which all run together. I'm planning to simplify it down to just "computeds", both for clarity and consistency with the model. Also, if the observable system gets standardized into a mixin, then they'll really have consistency.

Thanks for your input on this. It's always a crazy road when trying to formalize a science project into a real tool.

jpurcell001 commented 11 years ago

So for 1), I'm finding that it's pretty easy to just embed a state model directly into the view instance and then include that as a binding source, per one of your earlier suggestions. We didn't do that before as we just passed most static configuration information (view specific configuration, often changed by particular instances or subclasses at initialization time, but typically static over the lifespan of a particular view instance) via templateHelpers and serializeData, but now it's often easier to just use the data binding in the template instead. Will be interesting to see where it goes as places where I thought we would be using view computeds, we're now using an embedded view-specific model instead and just baking that into our view base classes (thus the naming conflict with get, and why it would be nice if it had a different name)

2) We had views that already exposed state information via template helpers; our initial use case for bindingComputeds as a function was to just re-expose that state (instead of having to re-enumerate each property). But we're now just representing that state as a model. The other main use case to represent as a function was for iterating up the prototype chain and creating a merged hash of all keys, including those from any superclasses. In any event, it's just a convenience; you can always do a `this.bindingComputeds = _.result(this, 'bindingComputeds') before you invoke applyBindings.

On binding contexts, here's a couple questions (probably don't belong in this github issue):

gmac commented 11 years ago

RE Bullet #1: no, there isn't a way to bind to non-exist properties, simply because they do not exist during the view's enumeration pass of all sources which catalogues context data. One approach would be to parse bindings first and then map those back to sources, although I feel this would be significantly more difficult and more unreliable in practice (especially when binding to multiple sources). I guess my feeling here is that model defaults are the proper approach. A model should be a known structure by design for many reasons, and if you do need to manage dynamic properties, then templated data binding doesn't seem like a natural display approach to begin with. For the exception cases where a model would need to dynamically restructure (personally, I can't think of any off the top of my head), there's always the option to re-apply bindings to the view.

RE Bullet #2: Yeah, I think I see where you're going with this. When you say "global scope", you're referring to un-prefixed property names within the binding context, correct? Two aspects to this response – first, plans: my general feeling is that what the view really needs is a designated intermediary structure to act as a ViewModel, per the very traditional approach to this sort of view system. If a view were to define viewModel as a first-class property (assigns itself to the view when passed in through constructor options), and that viewModel also surfaced its natural attribute names within the binding context, then there's an officially sanctioned place for managing view specific data. This could eliminate the need for defining computeds directly on the view. So... that's a working thought on where this may be going. Otherwise, I am curious – what are your feeling on the prefixed attributes of additional sources? Is this necessary, or being overly cautious with the design?