gmac / backbone.epoxy

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

Collection binding using the wrong context to search for binding sources #66

Closed tliao closed 10 years ago

tliao commented 10 years ago

I found a situation where a collection binding is throwing an exception because a parent view does not have the binding source.

I have a child view (TestView) whose el includes a collection$collection binding. TestView sets collection in the constructor. When the parent view (AppView) creates a new instance of TestView (i.e. new TestView()), Epoxy tries to apply the bindings. However, since AppView does NOT have collection set, I get the following:

Uncaught Error parsing bindings: "collection:$collection"
>> ReferenceError: $collection is not defined

I would have expected the applyBindings call on construction of TestView to be in the context of TestView rather than AppView.

Let me know if there's anything else I can do to help. I created a small project that demonstrates this if you need it and I can also provide a full stacktrace.

tliao commented 10 years ago

After looking at it some more, I think this is correct (albeit inconvenient) behavior. I was rendering TestView's el into AppView during initialize. AppView calls applyBindings as the last step in its construction. Since the binding was in the html, AppView finds the binding in its el and tries to bind it again.

adamyonk commented 10 years ago

I'm running into this as well - did you find a good solution for this case?

maxpoulin64 commented 10 years ago

@adamyonk: Here's what I use personnaly:

Epoxy.binding.addHandler('view', {
    init: function($element, view) {
        this.$original = $element;
        this.$view = $element;
    },

    set: function($element, view) {
        var $newEl = view ? view.$el : this.$original;

        // We use .after() + .detach() to avoid jQuery 
        // unbinding Epoxy's handlers on the child view
        this.$view.after($newEl);
        this.$view.detach();
        this.$view = $newEl;
    },

    clean: function() {
        // Will cause "view" to be null so set() will put back the original element
        this.set(null, null); 
    }
});

It's a custom handler that just replaces the element it is bound to with the actual View. The view has to be in the binding context for this to work (inside a model or directly in bindingSources). Since the bindings of the parent view are already queried the problem is avoided and they will never interfere.

tliao commented 10 years ago

@adamyonk: It's been a while and I can't find my sample project, so this is a little hand-wavy. The general idea was to not insert the child view's el onto the parent view's el until after the parent view completes initializing (which will call applyBindings()).

I instantiate all the views in initialize, but don't render anything onto the page yet. Initialize also sets up a callback on the model to render on some event firing. Parent views call their child view's render and add the child view's el to its own el. Since the epoxy bindings have already been applied, this issue is avoided.

ParentView.js

Epoxy.View.extend({

    initialize: function() {
        // Only instantiate the child view, do *not* render the child view.
        this.childView = new ChildView();
        // Causes the render to be triggered separately from the initialization of the parent view
        this.model.on('change:id', this.render, this);
    },

    render: function() {
        // Both render the child view *and* put the child view's el onto the parent view's el
        this.$('#child-view-container').html(this.childView.render().el);
        return this;
    }

});

ChildView.js

// No render on instantiation
Epoxy.View.extend({

    render: function() {
        // Whatever rendering you need to do here.
        return this;
    }

});
adamyonk commented 10 years ago

Thanks for the recommendations! I ended up just mixing in the Epoxy.View to a regular Backbone.View, so that I could call applyBindings() before the parent view's initializer is finished.

dharchouhan commented 9 years ago

@maxpoulin64 : I know it is long time you have posted this solution but if possible can you give an example on how to use this handler