Search code examples
javascriptbackbone.jskeyup

Best practice to maintain cursor position when keyup triggers save


I'm using Backbone.js. In my view, I have a textarea whose keyup is bound to a function like this (but see edit below):

this.model.save({text: self.$('textarea').val()}, {patch: true});

In the view's initialize function, I bind the model's change event to the view's render function:

initialize: function() {
  this.listenTo(this.model, 'change', _.bind(this.render, this));
},

Trouble is, when the user types in the textarea, the following sequence of events occurs:

  1. The keyup event fires.
  2. The keyup handler calls save on the model.
  3. The call to save triggers the model's model's change event.
  4. The view, listening for the model's change event, calls render.
  5. The textarea is replaced in the DOM.
  6. The textarea is no longer focused, and the text cursor position is lost.

What is the best practice for situations like this, where a texarea's keyup event needs to trigger a sync? Some options I have considered:

  1. Don't bind change to render. Disadvantage: If the model data changes due to anything other than the user typing, the textarea doesn't automatically update.
  2. Read and remember the cursor position at the beginning of render. Set the cursor position at the end of render. Disadvantage: Depends on cursor manipulation features for which browser support is spotty.
  3. In the keyup handler, set a temporary property on the view telling it not to re-render. Unset it after the model has been saved. Disadvantage: Feels like spaghetti code, fights against the structure of Backbone.

Are there any options I'm not seeing? Do you recommend one of the options above?

Edit:

I didn't want to distract from the main point, but since it came up in one of the answers: I'm not binding directly to keyup, but intermediating it with _.debounce. Thus, the event handler only runs once the user stops typing, as defined by a certain amount of time elapsing since the last keyup.


Solution

  • First of all I'd like to discourage this as it seems like really strange behaviour to save your model on keyup. If there is a use-case which really necessitates this I'd suggest using the input event at the very least - otherwise you'll end up saving the model every time the user presses even an arrow key, shift, ctrl etc.

    I think you'll also want to debounce the input events by 500ms or so you're not actually saving the model every single keystroke.

    To address your comment in point 1:

    Disadvantage: If the model data changes due to anything other than the user typing, the textarea doesn't automatically update

    You need to ask yourself the likelihood of this happening and how important it is that the view is rerendered if this was to happen.

    Finally, if you decide that this is indeed likely and it is important that the view is rerendered, then you can try something like this

    http://jsfiddle.net/nuewwdmr/2/

    One of the important parts here is the mapping of model attribute names to the name field of your inputs. What I've done here follows the sequence of events you described above. The difference is that when the model changes, we inspect the changed attributes and update the value of the corresponding element in the template.

    This works fine in a very simple situation, the happy path, where the user is typing in a "normal" way into the input. If the user, however, decides to go back to the start of the input and change a letter to capitalize it, for example, the cursor will jump to end of the string after the change event in the model occurs.

    The behaviour you require here is really two-way data-binding which is by no means trivial, especially with Backbone given just how little functionality a Backbone View has.

    My advice would be your point 1

    Don't bind change to render

    Edit

    If you want to look further into model / view binding you could take a look at two libraries:

    stickit

    epoxy

    I've used stickit before and it's...fine. Not great. It's ok for simple bindings, for example binding a "top-level" model attribute to an input element. Once you get into nested attributes you'll run into problems and you'll then have to look into something like Backbone Deep Model.

    Like I said, Backbone's View doesn't offer very much. If you've got the time I'd suggest looking into using React components in place of Backbone Views, or even look at some of the interesting stuff that ampersand have to offer.