gmac / backbone.epoxy

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

Support for template data that isn't in the DOM #27

Closed nfm closed 9 years ago

nfm commented 11 years ago

First up, I'm loving epoxy.js - it rocks. Thanks!

I'm just getting started with it so I'm not sure if I'm doing it wrong, but I'm having to work around a few issues that seem to stem from the assumption that the template will already be in the DOM, or that the view's el will be set to an existing DOM element.

Here's a simplified version of the kinds of views I'm writing:

class MyView extends Backbone.Epoxy.View
  template: MyTemplates['foo/bar']
  render: ->
    @$el.html(@template(model: @model))
    this

So the template is a function (I'm using haml_coffee_assets but I expect this is an issue with any template compilation). In the above example, if I have data-bind attributes in the template, I need to call @applyBindings() after populating @$el for epoxy to do its thing.

I hit another problem when binding collections - adding to the collection ends up running something like $element.append(view.$el) - and el won't have ever been populated from my template, so I get an empty div appended.

In the docs, most of the views seem to either target an existing DOM element in the page (el: '#some-id') or use an inline template (el:"<li><input type='checkbox'> <input type='text' class='todo'></li>").

Have I missed the correct way to handle this? Is this a valid use case to support, or am I about to go down a bad path here?

Thanks!

gmac commented 11 years ago

Hey, thanks for the feedback. At the moment, Epoxy does rely on the template existing within the element's DOM. This is a really tricky situation that I fought with while setting it up, and I do agree that the final outcome is only mediocre.

The fundamental issue here is where the template text comes from, and how it gets provided to a binding. Obviously, we can't put a template into a binding declaration; that would be craziness... so, the options are that we lift the template from the element (which is how this is implemented), or else we provide a reference to the template text through the binding. To do that, we're reliant on some kind of model field to hold the template... which is generally where Epoxy's viewModel idea is headed: a sanctioned model for holding view data.

If you have any suggestions here on how you'd like to be able to better use templates, I'm all ears for an API enhancement.

nfm commented 11 years ago

Thanks for the detailed reply @gmac.

I'm not sure if I have anything particularly insightful to add - I'm still getting my head around the epoxy.js internals.

My initial thought would be to override the view's render function instead of the constructor function, use the same super handling there as in the current constructor override, and call applyBindings() last, so it would be the user's responsibility to populate $el in their render function. Then the collection binding handlers could be modified to call before and append with view.render().$el, instead of just view.$el.

Of course, this approach would break the case where the template is already within the element's DOM, and the user never calls render() because they don't need to. There are probably other complexities at play here that I haven't picked up on too!

Prinzhorn commented 10 years ago

I'm not sure if it helps in your case, but here's what I'm doing: Backbone allows the el property to be a function. You can return your template string from this function.

Actual example:

var templateCache = {};
var template = function(name) {
    return function() {
        return templateCache[name] || (templateCache[name] = $('template#' + name).html());
    };
};

views.TodoItemView = Backbone.Epoxy.View.extend({
    bindings: 'data-bind',
    el: template('todoItemTmpl')
});

This way the template will be looked up right when the view is instantiated. Backbone will wrap the string in a jQuery object, which will cause the outer most/first element to become the element of the view.

Edit: You can see this here in the Backbone code

http://backbonejs.org/docs/backbone.html#section-129

Ensure that the View has a DOM element to render into. If this.el is a string, pass it through $(), take the first matching element, and re-assign it to el. Otherwise, create an element from the id, className and tagName properties.

gmac commented 9 years ago

Awesome, I like what @prinzhorn said. Let's go with that. While it's a little kludgy, Epoxy is specialized enough that its goal is not to be the ultimate template renderer (especially now, with tools like React available).

boussou commented 8 years ago

just a comment for the record.

I worked on this problem too, for collections (the binding). I think that, to keep the BB spirit, el should be the result of a call to render()

currently applybinding() actually takes the el as it is, not giving a change to the view to change the content. (maybe calling it in the constructor is too early?)

regarding the cache handler, the best would be to borrow the Marionnette's which is perfect. you can override the tmplaite engine & the loading method. That's what I use.

  http://marionettejs.com/docs/marionette.templatecache.html