JqGrid makes our tablework easier on the web, but the original solution is obtrusive. Let’s see how we can polish it.
First, we have to install 2dc_jqgrid, and install it, according to 2dconcept’s document. I didn’t want to install squirrel, because I already added searchlogic, but since squirrel has will_paginate’s functionality built in, we have to deal with it ourselves.
Then, let’s pick an already written CRUD, let’s call ‘users’, which is tested from the UI side (eg. have a test with Webrat / Selenium / anything similar). This is important: making an unobtrusive page needs testable functionality.
First, let’s replace the original table with JqGrid:
index.html template (this is haml, but I think it’s readable):
%table#users
%tr
%th= User.human_attribute_name(:username)
%th= User.human_attribute_name(:email)
%th= User.human_attribute_name(:tel)
- @users.each do |user|
%tr
%td= h(user.username)
%td= h(user.email)
%td= h(user.tel)
%td
= link_to t(:show), user
= link_to t(:edit), edit_user_path(user)
%tr
%td{:cols => "5"}= will_paginate
:ruby
haml_concat jqgrid(t(".title"), "asdf", "/users",
[
{ :field => "id", :label => "ID", :width => 35, :resizable => false },
{ :field => "username", :label => User.human_attribute_name(:username), :width => 100 },
{ :field => "email", :label => User.human_attribute_name(:email), :width => 140 },
{ :field => "company", :label => User.human_attribute_name(:company), :width => 140 },
{ :field => "tel", :label => User.human_attribute_name(:tel), :width => 80, :align => "center" } ],
{ :height => 220, :selection_handler => "users_select_row", :direct_selection => true })
%p#newuserlink= link_to t(".new"), new_user_path
#userform.popup
It seems height setting was needed because of some browsers. Selection_handler and direct_selection setting are for future extensions. <div id=”userform” class=”popup”>…</div> will be your popup edit box.
So far so good, let’s give JqGrid some data:
UsersController#index:
def index
@users = User
@@index_columns ||= [:id, :username, :email, :company, :telephone, :mobile]
if params[:_search].present?
if params[:_search] == "true"
filters = {}
@@index_columns.each do |param|
filters[("#{param}_like").to_sym] = "%#{params[param]}%" if params[param].present?
end
@users = User.search(filters)
end
if params[:sidx].present?
@users = if params[:sord] == "desc"
@users.public_send("descend_by_#{params[:sidx]}")
else
@users.public_send("ascend_by_#{params[:sidx]}")
end
end
end
@users = @users.paginate(:page => params[:page], :per_page => params[:rows] || 10)
respond_to do |format|
format.html
format.json do
render :json => @users.to_jqgrid_json(
@@index_columns, params[:page], params[:rows], @users.total_entries
)
end
end
end
As you can see, we kept html output as is, but we added extra functionality jqgrid search and filter. It’s a bit more verbose than with Squirrel, but that’s alright. Now your page should turn the original table to a grid if you have javascript enabled. Let’s handle our first action: open an in-page popup for editing an entry:
index.html template, annotated:
For first, tell jQuery to ask for Javascript responses to ajax requests:
jQuery.ajaxSetup({
'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")}
})
Then, register events when the page is loaded. Start with New User link in the bottom. Note that inject_userform_submit is called after the response is arrived: we can’t register the edit form’s submit event until it’s loaded, we have to inject the code after we get it loaded.
$(function() {
$("#newuserlink").click(function() {
$.get("/users/new", null, inject_userform_submit, "script");
return false;
})
This is the meat of our popup: I use jQuery Tools for that.
$(".popup").overlay({
expose: {
color: '#fff',
loadSpeed: 100,
opacity: 0.5
}
})
})
Let’s handle JqGrid’s row selection callback too, just like #new:
users_select_row = function(id) {
$.get("/users/"+id+"/edit", null, inject_userform_submit, "script")
}
Last but not least replace the popup edit/new form’s submit to handle things in Javascript, and make this popup opened:
inject_userform_submit = function() {
$("#userform form").submit(function() {
$.post($(this).attr("action"), $(this).serialize(), null, "script");
return false;
})
$(".popup").overlay().load()
}
Now we have requests, let’s create responses. I really hope everybody puts the form genrator in ‘form’ partial, and we can reuse it:
new.js.erb:
$("#userform").html("<%= escape_javascript(render 'form') %>")
edit.js.erb:
$("#userform").html("<%= escape_javascript(render 'form') %>")
create.js.erb:
<% if flash.has_key?(:notice) %>
$("#flashes").html('<div class="flash notice"><%= escape_javascript(flash.delete(:notice)) %></div>')
$("#users").trigger("reloadGrid")
$(".popup").overlay().close()
<% else %>
$("#flashes").html('<div class="flash error"><%= escape_javascript(flash.delete(:error)) %></div>')
$("#userform").html("<%= escape_javascript(render 'form') %>")
<% end %>
You can make it more elegant if you have another flash div if you re-render the form.
Then, we have to get UsersController#create and UsersController#update behave differently:
UsersController#create:
...
if @user.save
flash[:notice] = t('users.create.success')
respond_to do |format|
format.html { redirect_to users_path }
format.js
end
else
flash.now[:error] = t('users.create.failure')
respond_to do |format|
format.html { render "new" }
format.js
end
end
...
UsersController#update:
def update
@user = User.find(params[:id])
if @user.update_attributes(params[:user])
flash[:notice] = t('users.update.success')
respond_to do |format|
format.html { redirect_to users_path }
format.js { render "create" }
end
else
respond_to do |format|
format.html { render "edit" }
format.js { render "create" }
end
end
end
Then, of course, you have to do some CSS magic to make it nice:
.popup {
width: 408px;
display: none;
border: 2px solid #666;
background-color: #324; }
Or, in SASS:
.popup :width 408px :display none :border 2px solid #666 :background-color #324
We have achieved our goals:
- the original code is still working (run the tests again, you can measure this)
- the previous one vice-versa: our tests are still greens
- we used our old gems (searchlogic, will_paginate)
- the code is unobtrusive, everything works fine if JavaScript is not enabled
Enjoy!