14
votes

I have a simple view that binds itself to redraw when the model changes as most guides indicate:

this.model.bind('change', this.render, this);

This view has a form with an input box and a submit button. I am binding to the 'change' event of the input box to change the related item on the model. (I can do this manually or with the ModelBinder project, works the same either way.)

A user changes the value in the form input box, then clicks on the submit button. The model is updated, the view and form are re-rendered. The submit button's click event is squashed.

I'm using Backbone's events property:

events : {
    'submit form' : 'save'
},

I know that the event is getting ignored because the DOM element that was clicked on is no longer there. I can place a small setTimeout inside of render() to prevent the HTML from being swapped out and things work as expected, but this requires a wait.

I can't be the first person to struggle with this - what's the standard way of capturing form 'change' events, updating the model, and redrawing the view without losing some key click or keypress information?

Similarly if I have several form items the user is unable to tab between the items after changing the contents as the form is redrawn.

Update 1 - 3 Days later

Still trying to find a good solution. Things I've tried:

  • moving or cloning the contents of the view to a different area on the page. The click event is still never received.
  • registering the click event with $(document).on or $(document).live instead of the standard view events object
  • separating out the form so that the entire form (inputs and buttons) stays together without being redrawn. Redraw parent elements (which rely on form values) and re-insert already drawn form. This fixes the related issue of being unable to tab across an element, but doesn't fix click events.
  • works as desired in firefox 4, but not ie9 or chrome. *

Update 2 - With example code

One of the comments asked for some code. I've massively simplified the code into one page and included it below. The actual application is much more complicated. With code as simple below I could just manually re-render parts of the page on change. In the actual application I'm using dustjs templates and even if I don't re-render the form, but do re-render elements that contain the form I have issues clicking on the submit. I'm hoping for a 'pattern' that is typical for backbone applications, including complicated pages and models and forms.

Most of the 'demo' apps and even the sites that I've seen using backbone appear to be mostly presentation focused apps that don't actually gather a lot of input from the user. If you know of a good data-collection focused application/demo based on backbone that would be helpful.

Code:

<!doctype html>
<html lang="en">
<head>
    <script src="libs/jquery.js"></script>
    <script src="libs/underscore.js"></script>
    <script src="libs/backbone.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/template" id="edit_user_template">
    <h3>Edit User <%=id%> : <%=name%></h3>
    <form>
        Name: <input type="text" name="name" value="<%=name%>"><br/>
        Email: <input type="text" name="email" value="<%=email%>"><br/>
        <input type="submit">
    </form>
</script>
<script>
    var UserModel = Backbone.Model.extend({});
    var model = new UserModel({id: '1', name:'Joe', email:'[email protected]'});
    var UserView = Backbone.View.extend({
        events : {
            'submit form' : 'save',
            'change input' : 'inputChange'
        },
        initialize: function(){
            this.model.bind('change', this.render, this);
        },
        render: function(){
            var template = _.template($('#edit_user_template').html(), this.model.toJSON());
            this.$el.html(template);
        },
        inputChange : function(evt){
            var target = $(evt.currentTarget);
            this.model.set(target.attr('name'), target.val());
        },
        save : function(event){
            console.log('saving');
            event.preventDefault();
            //this.model.save();
        }
    });
    var view = new UserView({model: model, el: '#container'});
    view.render();
</script>
</body>
</html>
5
I don't know what your render() method is doing but it sounds like it's removing then adding the form elements each change. Have you tried just updating the form values and leaving the elements on the DOM? - JamesOR
My render is calling this.$el.html() with the results of a dustjs template. - Will Shaver
you can use the js template to render the first time, then after initializing the view, just update the individual form values every render after that so you aren't removing/adding form elements. - JamesOR
Do you need to re-render the form every time something changes in the model? Or if there's a part of the form that changes when the user types something, could you use a subview that listens to the model? - Oiva Eskola
Even if I don't re-render that form, if there's part of the page that contains the form it still breaks. I can even detach the form, re-render the page, re-append the form. Same deal. - Will Shaver

5 Answers

1
votes

You ask for a pattern in backbone. The pattern is to use models to hold form info (like you are doing) and then update specific elements on the page instead of re-rendering everything every time the model changes. ModelBinder offers a nice way to do all of that (https://github.com/theironcook/Backbone.ModelBinder).

I don't think you are going to find a way around not being able to submit a form that doesn't exist anymore. You need to accomplish what you want in a different way.

1
votes

If I understand correctly, what you need to do is to split your view into 2 smaller views. One will re-render upon change, the other will display form itself.

Depending on your case, you may or may not want also third view to bind those 2 together.

Side comment:

When you use MVC design pattern, it tends to be recursive. I think this is the case. Think of the data in the form as a model, the re-rendering view as a 'view' part of MVC, and the form view as a 'controller'.

0
votes

Do not quite understand what you're trying to do, watching the code I understand that you want to simulate knockoutjs's binding model, which is awesome.

Anyway, you can get a similar «effect» if you divide into two views that operate on the same model. One updates the model, while the other, observed changes and auto-updates.

Here is the code:

<!doctype html>
<html lang="en">
<head>
    <script src="libs/jquery.min.js"></script>
    <script src="libs/underscore.min.js"></script>
    <script src="libs/backbone.min.js"></script>
</head>
<body>
    <div id="header"></div>
    <div id="container"></div>
<script type="text/template" id="user_template">
    <h3>Edit User <%=id%> : <%=name%></h3>
</script>
<script type="text/template" id="edit_user_template">
    <form>
        Name: <input type="text" name="name" value="<%=name%>"><br/>
        Email: <input type="text" name="email" value="<%=email%>"><br/>
        <input type="submit">
    </form>
</script>
<script>
    var UserModel = Backbone.Model.extend({});
    var model = new UserModel({id: '1', name:'Joe', email:'[email protected]'});

    var UserView = Backbone.View.extend({
        initialize: function(){
            this.model.on("change", this.render, this);
        },
        render: function(){
            var template = _.template($('#user_template').html(), this.model.toJSON());
            this.$el.html(template);
            return this;
        },
    });

    var FormView = Backbone.View.extend({
        events : {
            'submit form' : 'save',
            'keyup input' : 'inputChange'
        },
        render: function(){
            var template = _.template($('#edit_user_template').html(), this.model.toJSON());
            this.$el.html(template);
            return this;
        },
        inputChange : function(evt){
            console.log('change');
            var target = $(evt.currentTarget);
            this.model.set(target.attr('name'), target.val());
        },
        save : function(event){
            console.log('saving');
            event.preventDefault();
            //this.model.save();
        }
    });

    var user = new UserView({model: model, el: '#header'});
    user.render();

    var form = new FormView({model: model, el: '#container'});
    form.render();
</script>
</body>
</html>
0
votes

There should be two possible solutions to your issue:

  1. Bind to click on the submit input, instead of the submit event itself.

    events: { 'click input[type=submit]' : 'save' }

    This should survive the complete replacement of the html.

  2. Re-delegate events manually after you render. This is usually necessary if you are re-rendering a view that has sub-views, but it would apply here too.

    this.delegateEvents()

0
votes

I don't know if I would rely completely on backbone to solve your problem. I think this is one of those situations where backbone allows you to fill in with your own code. You could modify the templates a little and listen to changes in the model and then decide weather a view needs to be re-rendered, or just updated. I don't think i would try re-rendering the forms parts unless the model is reset

<!doctype html>
<html lang="en">
<head>
<script src="libs/jquery.min.js"></script>
<script src="libs/underscore.min.js"></script>
<script src="libs/backbone.min.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/template" id="edit_user_template">
    <h3>Edit User <span class="f-id"><%=id%></span> : <span class="f-name"><%=name%></span></h3>
    <form>
        Name: <input type="text" name="name" value="<%=name%>"><br/>
        Email: <input type="text" name="email" value="<%=email%>"><br/>
        <input type="submit">
    </form>
</script>
<script>
    var UserModel = Backbone.Model.extend({});
    var model = new UserModel({id: '1', name:'Joe', email:'[email protected]'});
    var UserView = Backbone.View.extend({
        events : {
            'submit form' : 'save',
            'change input' : 'inputChange'
        },
        initialize: function(){
            //you could have the change event trigger updates instead of re-rendering the form
            //this.model.bind('change', this.update, this);
        },
        render: function(){
            var template = _.template($('#edit_user_template').html(), this.model.toJSON());
            this.$el.html(template);
        },
        inputChange : function(evt){
            var target = $(evt.currentTarget);
            this.model.set(target.attr('name'), target.val());
        },
        update:function(){
            // you could trigger other things here if you need other views to know what is going on.
            $(".f-id").text(this.model.get("id"));
            $(".f-name").text(this.model.get("name"));          
        },
        save : function(event){
            console.log('saving');
            this.update();
            event.preventDefault();
        }
    });
    var view = new UserView({model: model, el: '#container'});
    view.render();
</script>
</body>
</html>