Loading form on a separate webpage, to make edits to a record so you can redirect user back to the previous screen once she is finished, with the full page reload is so 20th century. In 21st century to avoid that behaviour in our applications we use inline edits. Despite the fact that technique is totally awesome (compared to the other techniques available if you are not on a client side MVC), maintaining javascript files behind it can (and in most cases will) become a nightmare.

To make my life easier while working on projects where I am still forced to use vanilla Rails stack, I came with solution I will be talking about today. Otherwise, my advise to you is full steam ahead towards the client side MVC framework — it will make your life as developer easier.

Here is what I will try to do:

Create HTML markup to hold our form, Create Javascript form object which will be represented by a custom jQuery UI Widget. Inside it we will encapsulate all form logic and handle all form events.

Javascript all the things!

First thing you need to realize is that javascript goes hand to hand with your HTML markup. Messy HTML markup equals messy javascript. Period. In our case we are going to have a container which will hold both HTML representation of a form and the record we are editing itself:

<div data-behavior="parent">

<div data-behavior="expandable">

<p>

Some text that needs to be edited

</p>



<button data-behavior="toggle-form">

Edit

</button>

</div>



<div class="hidden" data-behavior="expandable form">

<!-- Render your form here or leave blank if you plan to

load your form remotely -->

</div>

</div>

Pretty straightforward: we have some text and Edit button inside a container to glue them together. Now we need to make sure that whenever user clicks on the Edit button, the text is hidden and the form is displayed. We will do this by creating a formObject (which will handle all the toggling logic) from specified markup once the user click the button:

$(document).on("click", "[data-behaviour~=toggle-form]",

function(event) {

event.preventDefault();



formIsCreated = function() {

$(this).closest("[data-behavior~=parent]").

is(":data('ui-formObject')");

};



if(!formIsCreated()) {

$(this).closest.formObject();

}

});

Here we are just preventing the default event (just to be sure no magic happens unless we want it to), validating wether the form object is already created and if the validation passes we create the form object on the element that behaves as parent.

We came to the point where we need a widget called formObject. Lets go ahead and create it using jQuery UI widget factory:

(function($, undefined_) {

return $.widget("ui.formObject", {

_create: function() {

this._on({

});

},



_destroy: function() {

},



_keyup: function(event) {

},



_keydown: function(event) {

},



_toggle: function() {

},



_validate: function() {

}

});

})(jQuery);

This is a basic widget layout with six private methods defined. But what are they for?

Lets start with the _create() method, which is called by default once we create our formObject() by calling $(selector).formObject() — think of it as of widget’s constructor:

_create: function() {

this.element.addClass("ui-form-object");

this._validate();

this._toggle();



this._on({

"click a[data-behavior~=cancel]": function(event) {

event.preventDefault();

this.destroy();

},



"keyup input": "_keyup",

"keydown input": "_keydown"

});

}

Inside the create method we are doing several things:

providing widget with some technical details (such as ID and class name; latter will be used to determine wether widget is created or not), validating the form, binding different events to our form.

Yes, that’s right — you can bind any event toggled in you widget or document here and make your formObject() react to it. Unfortunately, we can’t use Rails ajax events (like ajax:beforeSend and ajax:success due to a default behaviour of jQuery UI. To make this work either override a method inside jQuery UI, wait for a fix or register global handlers for desired events and retrigger them with different names without colon inside. Also notice that we bound a click event to the element with the behaviour of cancel; this way we are expecting our form to contain such element — don’t forget to include it in your form.

Next, our _create() method is calling _validate() and _toggle() methods:

_toggle: function() {

this.element.children("[data-behavior~=expandable]").

toggleClass("hidden");

},



_validate: function() {

// Place your validation logic in here. I append data-valid

// attribute to the formObject and set it to either true or

// false for form being valid or not.

},

We need to call _validate() function here because if user is creating a new record, our form will be empty and invalid; so if we are invalidating the form by disabling the submit button, we need to disable it right away.

Inside our _create() method, we are also calling for destroy(), _keyup() and _keydown() events. Lets start with destroy(); we didn’t specify the public method as you’ve may noticed. It is not needed because jQuery Widget offers you one right out of the box. It removes the widget functionality completely and then delegates out to our _destroy() for custom, widget-specific, cleanup.

_destroy: function() {

this.element.removeAttr("class");

this.element.removeAttr("data-valid");

this._toggle();

// I am also cleaning up the form in here — either by

// cleaning out an entire element with a behaviour of form or

// by cycling through inputs and erasing them

},



_keyup: function(event) {

switch (event.keyCode) {

case $.ui.keyCode.ESCAPE:

return this.destroy();

default:

this._validate(event);

}

},



_keydown: function(event) {

switch (event.keyCode) {

case $.ui.keyCode.ENTER:

if (this.element.attr("data-valid") === "false") {

event.preventDefault();

return false;

}

}

}

_destroy() method does nothing fancy — just cleans up the widget and returns it to its vanilla version. _key*() events are separated because different keystates for different keys are handled differently by different browsers under different operating systems (welcome to the 21st century which is different). Note that we are validating our form every time a user releases a key. Also, esc key is bound to destroy the formObject() once pressed. On enter we are submitting our form (only if it’s valid). Keep in mind that you can also specify different key-bindings to different type of inputs or elements, just add “keyup whatever”: “_functionName” listener to your _create() method.

This will work in case our form is local (i.e. it is rendered into to the HTML once the page is rendered). In case form is remote (it is available in a different location from one where user is), we need to fetch it and insert into our formObject().

Here we will take the advantage of the Rails framework and it’s UJS adapter. In case you are not using Rails, you will have to implement your own logic to fetch remote page. First lets alter our markup:

<div data-behavior="parent">

<div data-behavior="expandable">

<p>

Some text that needs to be edited

</p>



<a href="/edit" data-remote="true" data-behavior="toggle-form">

Edit

</button>

</div>



<div class="hidden" data-behavior="expandable form">

</div>

</div>

Now lets make Rails ajax events trigger global events:

$(document).on("ajax:success",

"a[data-remote][data-behavior~=toggle-form]",

function(event, data, status, xhr) {

$(this).trigger("successfullFormFetch", data);

});

Yes, I know, successfullFormFetch is a dumb name and ajaxSuccess would be better but it will conflict with Turobolinks (trollface.jpg). Now it’s only the matter of extending our _create() method and adding custom listener:

_create: function() {

this._on({

"successfullFormFetch": function(event, data) {

$(this).children("[data-behavior~=form]").html(data);

}

});

}

As you’ve probably guessed, we can use the same approach to handle remote form submit (again, if you are not using Rails, you will have to implement your own behaviour here):

$(document).on("ajax:success",

"form[data-remote,

function(event, data, status, xhr) {

$(this).trigger("successFormSubmit", data);

});

The _on() binding:

_create: function() {

this._on({

"successFormSubmit": function(event, data) {

// Logic to update the text. You can even rely on the

// standard Rails JS response here

$(this).closest("[data-behavior~=parent]").

data("ui-formObject").destroy()

}

});

}

Final thoughts

Using this method I’ve managed to get rid of spaghetti jQuery method calls scattered among my application. Now I have a single form.js file where I keep all the logic related to inline edits. It is easy to extend, it is easy manage. Plus with this technique I am getting uniform HTML markup for free.

But as I’ve said before, if you can, run towards client side MVC framework. It will do all the heavy lifting for you (like managing the inline edits).