Tuesday, November 1, 2011

Submitting Backbone.js Forms with Enter

‹prev | My Chain | next›

Up tonight I would like to address a minor annoyance in my Backbone.js calendar application. Specifically, needing to click on buttons in order to make form dialogs do things.

In regular HTML forms, wrapping input fields inside a <form> tag along with a submit button is sufficient to get Enter to submit the form. All browsers (yes, even Internet Explorer) recognize the Enter key as a signal to click the first submit button in the Form's DOM.

I used to find it amusing to find newbie code that had Javascript that intercepted event.keyCode ==13 so that the form could be submitted on Enter. But is seems that this is a best practice with Backbone forms.

The form that I use to filter appointments in my Calendar applications looks like:
    var CalendarFilter = Backbone.View.extend({
      template: template(
        '<input type="text" name="filter">' +
        '<input type="button" class="filter" value="Filter">'
      ),
      render: function() {
        $(this.el).html(this.template());
        return this;
      },
      // ...
    });
To do something with that form, I use Backbone View events to send click on the "Filter" button to the filter() method:
var CalendarFilter = Backbone.View.extend({
      // ...
      events: {
        'click .filter':  'filter'
      },
      filter: function(e) {
        var filter = $('input[type=text]', this.el).val();

        this.collection.each(function(model) {
          // This is a code smell.  See the chapter on custom
          // events in Recipes with Backbone to fix.
          model.trigger('calendar:filter', filter);
        });
      }
    });
If I want to filter when hitting Enter while in the text field, I need to do something like:
    var CalendarFilter = Backbone.View.extend({
      // ...
      events: {
        'click .filter':  'filter',
        'keypress input[type=text]': 'filterOnEnter'
      },
      filterOnEnter: function(e) {
        if (e.keyCode != 13) return;
        this.filter(e);
      },
      filter: function(e) { /* ... */ });
      }
    });
Unfortunately, there is no way to specify an Enter event in the events list, so I am forced to add an indirection in the form of the filterOnEnter() method. That method checks the key code of the keypress event. If the key code was anything other than Enter, then the guard clause returns immediately. If the key pressed was Enter, then I invoke the real filter() method.

Not ideal, but it works:

I do wonder, however, can I do something similar with a <form> field? To test this out, I alter the template to wrap the input elements in a <form> tag:
    var CalendarFilter = Backbone.View.extend({
      template: template(
        '<form>' +
        '<input type="text" name="filter">' +
        '<input type="button" class="filter" value="Filter">' +
        '</form>'
      ),
      render: function() {
        $(this.el).html(this.template());
        return this;
      },
      // ...
    });
That does not work as expected, however since the form is actually submitted when I hit enter (to the current URL, which is the default behavior for an empty <form> tag). I cannot bind a submit handler on that Form with the events attribute because the events are delegated jQuery events—by the time they kick in, the form would already be submitted regardless of trying to e.stopPropagation(). I could manually bind a submit handler in render(), but that is starting to seem like too much work for a solved problem.

Bah! I take some time to work through the remaining forms in my application to support key code 13. And I am somewhat satisfied—and coding like an HTML newbie!

Day #182

6 comments:

  1. i gotta ramp it up man. Your blogging all the stuff I've had to figure out on my own :D. Cant let you be the only decent backbone content out there.

    ReplyDelete
  2. The "submit form" event hash entry works - you just need to call event.preventDefault() instead of event.stopPropagation().

    Since event.target is the form itself, the goal is not to stop bubbling but to prevent the default action.

    Even though the events are bound in the delegate style, the sequence/bubbling order remains the same. The event capturing phase might be different, but that shouldn't matter here.

    ReplyDelete
  3. @Mark thanks! Let me know when you do start blogging -- I'm always eager for more Backbone resources :)

    ReplyDelete
  4. @Unknown Holy... freaking... crap.

    I made the same mistake back in September (http://japhr.blogspot.com/2011/09/event-propagation-in-backbonejs.html) and have been operating under the same false assumption for almost 2 months now!

    I cannot believe I made such a rookie mistake in a blog post in which I poke fun a newbies. Ah well, not the first time I've made a public idiot out of myself and it won't be the last :D

    ReplyDelete
  5. Chris, nice progress since I last checked your posts! I came in to say what unknown said, glad someone pointed that out. I need to get my code to a sharable state, but I should show you the stuff I did to make models define what fields are editable and what controls they use depending on the state of the model and then you use a standard form factory to pump out the form for a model.

    IE:

    New Task:
    Title(text box)
    Start Date(date picker)
    End Date (date picker)

    Edit Task:
    Title(text box prefilled in)
    Start Date(date picker)
    End Date (date picker)
    Percent complete (textbox, soon to use the slider widget from jquery UI).

    ReplyDelete
  6. In several applications I have added a custom jquery event - enterKey or similar and use it instead of keyup and keycode

    ReplyDelete