Search code examples
javascriptbackbone.js

Backbone subview event doesn't fire, access to this.model lost


I'm using a master view with a subview with it's own subview, and I am losing the event on the sub-sub view.

Looking in SO, it looks like a delegateEvents is needed, but I can't figure out how or where.

Also, it seems really hacky to have to pass tbody: tbodyEl[ "0" ].outerHTML to my template, but I don't know if it's related to the event issue.

Any help greatly appreciated.

mainView:

return Backbone.View.extend({
    el: "#content",
    initialize: function () {
        this.render();
    },
    render: function () {
        var todosView = new TodosView({
            collection: projectCol
        });
    }
});

todosView:

return Backbone.View.extend({
    el: "#content-body",
    template: _.template([
        '<div class="table-responsive">',
        '<table id="todo-table" class="table">',
        '<span class="caption">Top <%= project %> Tasks&nbsp;&nbsp;<a id="<%= project %>" class="projectName">(See All)</span></a>',
        '<thead>',
        '<tr>',
        '<th>Task</th>',
        '<th>Due</th>',
        '</tr>',
        '</thead>',
        '<%= tbody %>',
        '</table>',
        '</div>'
    ].join("")),
    initialize: function () {
        this.render();
    },
    render: function () {
        var projectName = this.collection.models[0].attributes.project;

        var tbodyEl = $("<tbody />");
        this.collection.each(function (item) {
            var todoView = new TodoView({
                model: item
            });
            tbodyEl.append(todoView.el);
        });
        this.$el.append(this.template({
            project: projectName,
            tbody: tbodyEl["0"].outerHTML
        }));
    }
});

todoView:

return Backbone.View.extend({
    tagName: "tr",
    className: "todo-rec",
    template: _.template([
        "<td>",
        "<label id='task' class='edit'><%= task %></label>",
        "<input id='edited-task' class='new-edit' style='display:none;' value='<%= task %>'>",
        "</td>",
        "<td>",
        "<span id='due' class='edit'><%= due %></span>",
        "<input id='edited-due' class='new-edit' style='display:none;' value='<%= due %>'>",
        "</td>",
    ].join("")),
    events: {
        "click .edit": "editFields"
    },
    initialize: function () {
        this.render();
    },
    render: function () {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    },
    editFields: function () {
        console.log("todoView: editFields clicked"); // <-- does not fire
    }
});

25 Oct Updated: Thanks to @WinterSoldier, this is what I ended up with: code build elements complete before inserting into DOM, and todoView.events works, giving full access to this.model, plus, it looks more 'Backbone-ish':

// mainView:
return Backbone.View.extend( {

    el:             "#content",

    initialize:         function(){
                    this.render();
                },

    render:         function(){
                    $( "#content-body" ).empty();

                    var section = new TodosView( { collection: projectCol } ); // should be returning a section div
                    $( "#content-body" ).append( section.el );
                }                           
} );


// todosView:
return Backbone.View.extend( {

    // used tagName so the element can be built complete before inserting into DOM
    tagName:        "div",
    className:      "table-responsive",

    // tbody changed to empty element
    template:       _.template( [
                    '<table id="todo-table" class="table">',
                        '<span class="caption">Top <%= project %> Tasks&nbsp;&nbsp;<a id="<%= project %>" class="projectName">(See All)</span></a>',
                        '<thead>',
                            '<tr>',
                                '<th>Comp.</th>',
                                '<th>Task</th>',
                                '<th>Due</th>',
                                '<th>Priority</th>',
                                '<th>Delegated To</th>',
                                '<th>Project</th>',
                                '<th>Del.</th>',
                            '</tr>',
                        '</thead>',
                        '<tbody id="tbodyContent"></tbody>',
                    '</table>' ].join( "" )
                ),

    initialize:         function(){
                    this.render();
                },

    render:         function(){
                    // new: render the template first, then append rows in #tbody
                    var projectName = this.collection.models[ 0 ].attributes.project;
                    this.$el.empty().html( this.template( {
                        project: projectName
                    } ) );

                    var this2 = this;
                    this.collection.each( function( item ){
                        var todoView = new TodoView( {model: item} );
                        this2.$el.find( "#tbodyContent" ).append( todoView.el );
                    } );
                    // now returning a <div class="table-responsive" with all rows
                    return this;
                }
    } );



// todoView:
// Note: nothing changed from original code

return Backbone.View.extend( {

    tagName:        "tr",
    className:      "todo-rec",

    template:       _.template( [
                        "<td>",
                            "<label id='task' class='edit'><%= task %></label>",
                            "<input id='edited-task' class='new-edit' style='display:none;' value='<%= task %>'>",
                        "</td>",
                        "<td>",
                            "<span id='due' class='edit'><%= due %></span>",
                            "<input id='edited-due' class='new-edit' style='display:none;' value='<%= due %>'>",
                        "</td>",
                     ].join( "" )
                ),

    events:         {
                    "click .edit":      "editFields"
                },

    initialize:         function() {
                    this.render();
                },

    render:         function() {
                    this.$el.html( this.template( this.model.toJSON() ) );
                    return this;
                },


    editFields:         function() {
                    console.log( "todoView: editFields clicked", this ); 
                }

    } );

Solution

  • let me list out the assumptions I made based on the question:

    • The template rendering has no issues.
    • The template code may be substituted with plain html to check the event firing.

    Based on those assumption, I created a collection with two models and passed them over to TodosView from which I iterated over those models to generate row view- there by adding it to 'tbody' tag.

    Please follow the fiddle here => https://jsfiddle.net/randomfifaguy/304kffed/1/

    console.log("hello");
    Test = Backbone.Model.extend({});
    var testModel1 = new Test({
      'task': 'taskA',
      'due': 'dueByZ'
    })
    var testModel2 = new Test({
      'task': 'taskB',
      'due': 'dueByY'
    })
    console.log(testModel1.get('task'));
    console.log(testModel2.get('task'));
    ModelCollection = Backbone.Collection.extend({});
    var models = new ModelCollection([testModel1, testModel2]);
    console.log(models);
    
    TodosView = Backbone.View.extend({
        el: "#content-body",
        initialize: function() {
          this.render();
        },
        render: function() {    
        var templ = "<div class='table-responsive'><table id='todo-table'class='table'><span class='caption'>Top Tasks&nbsp;&nbsp;<a id='<%= project %>' class='projectName'>(See All)</span></a><thead><tr><th>Task</th><th>Due</th></tr></thead><tbody></tbody></table></div>";
        $(this.el).html(templ);
        _.each(this.collection.models, function(mdl){
        	var view = new TodoView({
          model: mdl
        });    
        $('tbody').append(view.el);
        });    
      }
    });
    
    MainView = Backbone.View.extend({
    
        el: "#content",
    
        initialize: function() {
          this.render();
        },
    
        render: function() {
          new TodosView({
            collection: models
          });
        }
    });
    
    TodoView = Backbone.View.extend({
        tagName: "tr",
        className: "todo-rec",
        events: {
          "click .edit": "editFields"
        },
        initialize: function() {
          this.render();
          this.updateContent();
        },
        render: function() {
        var html = "<td><label id='task'class='edit tasklabel'></label><input id='edited-task'class='new-edit taskinput'style='display:none;' value=''></td><td><span id='due' class='edit duelabel'></span><input id='edited-due' class='new-edit dueinput' style='display:none;'value=''></td>"
        this.$el.html(html);
        return this;
        },
        editFields: function() {
          console.log("todoView: editFields clicked");
        },
        updateContent: function() {
          this.$el.find('.tasklabel').text(this.model.get('task'))
          this.$el.find('.taskinput').val(this.model.get('task'))
          this.$el.find('.duelabel').text(this.model.get('due'))
          this.$el.find('.dueinput').val(this.model.get('due'))
        }
    });
    
    var mainViewObj = new MainView();
    <body>
        <div id='content'>
          Here is a sample div
          <div id="content-body">
            content to be changed
          </div>
        </div>
    </body>

    However I'd like to know your html of the outermost view to help you better. Please compare it with the output html of the fiddle and let me know. Hope that answers your question.

    Answering your comment in detail You might want to change this piece of code

        //You might want to change this piece of code 
    
        var tbodyEl = $("<tbody />");
        this.collection.each(function (item) {
            var todoView = new TodoView({
                 model: item
            });
            tbodyEl.append(todoView.el);
        });
        this.$el.append(this.template({
                project: projectName,
                tbody: tbodyEl["0"].outerHTML
        }));

    Your tbodyEl basically points to nothing until you render try doing this before you append to tbody

        this.$el.append(this.template({
             project: projectName,
             tbody: tbodyEl["0"].outerHTML
        }));
       // followed by
    
        this.collection.each(function (item) {
             var todoView = new TodoView({
                  model: item
              });
              tbodyEl.append(todoView.el);
        });