Dynamically Add/Remove Items from jQuery Accordion
How to create and save dynamic lists of jQuery accordion items
By: Bruce Bahlmann - Contributing Author (your
feedback
is important to us!)
In rails development, there is occasionally a need to create a web interface
where some aspect needs to be dynamic and not known ahead of time. One
example is a custom survey tool where the number of questions a given survey
consists of will vary from survey to survey. Thus the actual number of
questions a given survey can support will vary. Approached from a relational
database perspective, this problem requires a complicated relationship among
tables to create and report on. However, with the help of MongoDB, creating
a survey tool with rails is much easier.
Assuming you already have MongoDB setup, we'd need to create the following
models to support our survey application using rails.

These models are pretty basic, but provide a wide range of surveys to be
created. Here are the contents of these models: First survey.rb which
includes array definitions for various drop down items provided. Two
important fields are mqid which contains the maximum question id in last
session (prevents duplication of question id for a given survey), and also
sorder, which is where the serialized aspect of sortable is stored (used to
recreate the order of the accordion upon edit).
[app/models/survey.rb]
class Survey
include MongoMapper::Document
QUESTIONTYPE = {0 => 'TextField', 1 => 'Dropdown', 2 => 'Checkbox', 3 => 'TextArea'}
OTHERHANDLING = {0 => 'None', 1 => 'Without otherText', 2 => 'With otherText'}
key :name, String
key :author, Hash
key :active, Boolean
key :audience, Hash
key :setting, Hash
key :mqid, Integer
key :sorder, String
many :questions
end
Then, question.rb which contains the parameters associated with individual
questions - the nice thing about using MongoDB is that these can change
without extensive programming such as would be required when using a
relational database.
[app/models/question.rb]
class Question
include MongoMapper::EmbeddedDocument
key :type, String
key :questionText, String
key :choices, Array
## Manditory handling: 0:Not, 1:Required
key :manditory, Boolean, :default => 0
key :fieldName, String
## Other handling: 0:None, 1:without otherText, 2:with otherText
key :otherHandling, String, :default => 0
key :otherText, String
belongs_to :survey
end
To execute this, you need several components from the jQuery UI library
which is available from the the following
site:
http://jqueryui.com/download Alternatively,
you can download the version of this I used just in case this site
becomes unavailable.
jquery-ui-1.8.13.zipInstalling these jquery modules
requires several steps, but to a rails developer (beginner or experienced),
it is easy to miss something so I've tried to keep this simple. Opening up
the zip, you will see the following files:

Given all these files, there are two different ways you can make this
work. The first (most difficult) is to place these files (themes and ui) into the following rails folders (stylesheets
and javascripts) as shown below:
 
The other way is just to use set references within these files and go with
that. Since this latter method is easier, we'll proceed with that instead of
using all the files from the download. Configuration happens in several places (application style sheet, the desired
view containing the list you want to use the jQuery mod, as well as the
controller).
In the application style sheet, rather than hard coding this resource to load
every time, you can make the load only happen when a user accesses a specific
page which requires this resource. To do this you need to use the yield feature
after you load defaults (which contain the jQuery resources). NOTE: Making
changes to the file below does require you to restart the rails server to apply
these settings.
[app/views/layouts/application.html.erb]
<head>
<%= javascript_include_tag :defaults %>
<% if content_for?(:head_include) %>
<%= yield(:head_include) %>
<% end %>
<% if content_for?(:javascript) %>
<script type="text/javascript" charset="utf-8">
<%= yield(:javascript) %>
</script>
<% end %>
</head>
Next, configure the form page: _form.html.erb

At that top of this file, add the required accordion resources and jQuery
configuration (to properly handshake with the application style sheet yield
commands described previously. Place these atop the file, just to keep it simple.
Using the "yield" command is a great way to keep the style sheet clean yet
allow individual view pages the capability to display complex content.
Beyond that include, what follows is the jQuery code that enabled the
accordion and sortable serialize to work correctly. These calls will be
explained further below.
[views/survey/_form.html.erb]
<% content_for :head_include do %>
<%= stylesheet_link_tag "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.9/themes/base/jquery-ui.css" %>
<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js" %>
<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.9/jquery-ui.min.js" %>
<% end %>
<% content_for :javascript do %>
function createQAccordion() {
// Dynamically create new question (accordion element upon button click)
$('#accordion').append('<div id="s_'+ cnt +'"><h3><a href="#">New Question</a></h3><div>'+
'<table border=0><tr><td>Question:</td>'+
'<td><input type="text" name="survey[question]['+ cnt +'][questionText]" value=""></td></tr>'+
'<tr><td>Question Type:</td><td><select id="survey_question_questionType" name="survey[question]['+ cnt +
'][questionType]"><option value=""></option><option value="0">Textbox</option><option value="1">'+
'Dropdown</option><option value="2">'+'Checkbox</option><option value="2">Textbox</option></select>'+
'</td></tr></table></div></div>')
.accordion('destroy')
.accordion({
header: "> div > h3",
alwaysOpen: false,
active: false
});
// Update sort order to reflect appended NEW question
var temp = $('#sorder').val();
if (temp == '') {
temp = 's[]=' + cnt;
} else {
temp = temp + '&s[]=' + cnt;
}
$('#sorder').val(temp);
// Update index count (prevent duplication)
cnt++;
$('#mqid').val(cnt);
}
function createBAccordion() {
// Dynamically create accordion element (page break rather than question)
$('#accordion').append('<div id="s_'+ cnt +'"><h3><a href="#">Page Break</a></h3><div>'+
'<input type="hidden" name="survey[question]['+ cnt +'][questionText]" value="PageBreak"></div></div>')
.accordion('destroy')
.accordion({
header: "> div > h3",
alwaysOpen: false,
autoHeight: false,
active: false
});
// Update sort order to reflect appended NEW question
var temp = $('#sorder').val();
if (temp == '') {
temp = 's[]=' + cnt;
} else {
temp = temp + '&s[]=' + cnt;
}
$('#sorder').val(temp);
// Update index count (prevent duplication)
cnt++;
$('#mqid').val(cnt);
}
$(window).load(function() {
cnt = <%= @survey.mqid %>;
// show/hide attributes of question based on dropdown selector
$("select[id*=questionType]").each(function() {
var div = $(this).attr('class');
if ($(this).val() == 1) {
$(".tarow"+div).show();
} else {
$(".tarow"+div).hide();
}
});
});
$(document).ready(function(){
var stop = false;
$( "#accordion h3" ).click(function( event ) {
if ( stop ) {
event.stopImmediatePropagation();
event.preventDefault();
stop = false;
}
});
$( "#accordion" )
.accordion({
header: "> div > h3",
alwaysOpen: false,
autoHeight: false,
active: false
})
.sortable({
axis: "y",
handle: "h3",
stop: function() {
stop = true;
},
update: function() {
// update sortable/serialize info upon change in order
$('#sorder').val($(this).sortable('serialize'));
}
});
// show/hide fields dependent on dropdown selector
$("select").bind('change', function() {
var div = $(this).attr('class');
if ($(this).val() == 1) {
$(".tarow"+div).show();
} else {
$(".tarow"+div).hide();
}
});
$("input[type=button][value=delete]").click(function() {
var parent = $(this).closest('div');
var head = parent.prev('h3');
// Update sort order to reflect deleted question
var temp = $('#sorder').val();
rstr = head.closest('div').attr('id').replace(/_/,'[]=');
temp = temp.replace(rstr,'');
// Account for manipulation of the sorted list
temp = temp.replace('&&','&');
temp = temp.replace(/^&/,'');
temp = temp.replace(/&$/,'');
parent.add(head).fadeOut('slow',function(){$(this).remove();});
$('#sorder').val(temp);
});
});
<% end %>
Below the necessary resource includes, there are a handful of function
calls. The first is the createAccordion call. This function is what we use
to dynamically create NEW accordion elements. To add more html form fields
within a given element, simply add to this createAccordion shell. There are
two different calls here. One is for adding a question the other is for
adding a page break - but they both work identically. Note, since adding an
element also modifies (albeit simply) the order of the accordion elements,
within this function comes some sort order code which tacks on sorting
information (so if a user saves this configuration, upon editing it in the
future, they will see the order of the questions as they remembered it).
This function is followed by other functions. The first ($(window).load(function()) reads
assigns the variable "cnt" to the value of the last question count upon
page load. We use cnt to assign each accordion element a unique id and
assigning cnt upon page load ensures we don't assign any two elements the
same id within a given survey. This load function also corrects any
show/hide dependency elements (such as those dependent on dropdown selects).
The remaining functions have to deal with interaction with the web page
once it has been loaded.
The first function ($( "#accordion h3" ).click(function( event )) enables the accordion, drag-n-drop sortable,
and serialize
functions for the accordion elements.
Finally the last function is used to delete
individual elements from the accordion. Note that upon deleting an element,
care must be taken to update the sortable/serialize sort order so when the
user edits the survey in the future, the elements will display correctly and
the deleted item(s) will be removed from the serialized sort order.
The next step involves creating the accordion upon page load (should the
user be editing an existing or previously created survey). The key to this
code working was building the correct field name and accessing the correct
element from the MongoDB data structure. Note, some fields represent nested
fields.
[views/survey/_form.html.erb]
<div id="accordion">
<% if ((@survey["sorder"].nil?)||(@survey["sorder"].empty?)) %>
<% else %>
<% @survey.sorder.split('&').each do |ti| %>
<% key = ti.sub('s[]=','') %>
<% value = @survey.question[key] %>
<div id="s_<%= key %>">
<% if value["questionText"] == "PageBreak" %>
<h3><a href="#">Page Break</a></h3>
<div>
<%= text_field("survey[question]["+key+"]", :questionText, :hidden=>true, :value => value["questionText"]) %>
<input type="button" id="rmItem_<%= key %>" value="delete" />
</div>
<% else %>
<h3><a href="#"><%= (value["questionText"] == "PageBreak" ? "" : "Q:") %>
<%= (value["questionText"] == "" ? "Not yet defined" : value["questionText"][0..30]) %></a></h3>
<div><table border=0>
<tr><td>Question:</td><td>
<%= text_field("survey[question]["+key+"]", :questionText, :class=> key, :value => value["questionText"]) %></td></tr>
<tr><td>Question Type:</td><td>
<%= select("survey[question]["+key+"]", :questionType, Survey::QUESTIONTYPE.collect {|k,v| [v,k.to_i]},
{ :selected => value["questionType"], :include_blank=>true}, :class=>key) %></td></tr>
<tr><td>Manditory:</td><td><%= check_box("survey[question]["+key+"]", :manditory,
{:checked => (value["manditory"] == "1" ? true : false)}) %></td></tr>
<% if value["questionType"] == "1" %>
<% if value["choices"].count > 0 %>
<% value["choice"] = '' %>
<% value["choices"].each do |choice| %>
<% value["choice"] = value["choice"] + choice + "\r" %>
<% end %>
<% end %>
<% end %>
<tr class="tarow<%= key %>"><td style="vertical-align: top">Choices:</td><td>
<%= text_area("survey[question]["+key+"]", :choices, :rows=>5, :value=>value["choice"]) %></td></tr>
<tr class="tarow<%= key %>"><td>Other:</td><td>
<%= radio_button("survey[question]["+key+"]", "other", "None",
{:checked => (value["other"] == "None" ? true : false)}) %> None
<%= radio_button("survey[question]["+key+"]", "other", "oo",
{:checked => (value["other"] == "oo" ? true : false)}) %> Option Only
<%= radio_button("survey[question]["+key+"]", "other", "owwi",
{:checked => (value["other"] == "owwi" ? true : false)}) %> w/ Write In
</td></tr>
</table><input type="button" id="rmItem_<%= key %>" value="delete" />
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
Finally, there is one change you need to make to the survey controller to
account for a case where the user deletes all the questions within a given
survey. When this happens, the parameters returned do not include the
question field (as all element of the hash have been deleted), so when you pass this to MongoDB during an update, it
assumes this field is unchanged. To force MongoDB's hand to delete a hash
field. The other change required has to do with the submission of the text
area field. When you submit this field what is passed to the controller ends
up being a string with carriage (\r) returns in it. However to store this
within an array, we need to split up this string and store it correctly.
[app/controllers/survey_controller.rb]
# PUT /surveys/1
# PUT /surveys/1.xml
def update
@survey = Survey.find(params[:id])
myhash = Hash.new
myhash = params[:survey]
if !(myhash.has_key?("question"))
myhash["question"] = Hash.new
myhash["mqid"] = 1
myhash["sorder"] = ''
end
# Fix question choices
myhash["question"].each do |key,value|
if value["questionType"] == "1"
ca = Array.new # default (if no choices defined)
if value["choices"] != ""
value["choices"] = value["choices"].gsub("\r\n", '[r]')
value["choices"].split("[r]").each do |choice|
choice = choice.sub(/^\s+/,'')
choice = choice.sub(/\s+$/,'')
ca.push(choice)
end
end
myhash["question"][key]["choices"] = ca
end
end
respond_to do |format|
if @survey.update_attributes(myhash)
If you followed all those steps correctly, you should see something like the
following. Note, the Add Survey Question button allows you to add individual
questions dynamically. Upon clicking on an accordion element, you can see
the contents of the question (its configuration parameters) as well as an
available delete button which allows you to remove the question. Updating
the survey allows you to save the changes.

Can Birds-Eye.Net help you or your Company?
Receive your Birds-Eye.Net articles and white
papers hot off
the presses by adding our RSS feed to your reader.
|
|