Birds-Eye.Net
All things broadband and more...
 
Web Birds-Eye.Net

What's New?

Ruby on Rails (RoR)
Programming Reference


Models
External database connections
Passing current_user into model
Passing object into model
Using static lookup tables
Validates IF
Validates MongoMapper

Views
Dynamically delete form element
Edit create nested data
HTML form field check_box
Layout jQuery datatable module
Select array
Select cascading via JS
Text_area Array
Text_area listing submit
Text field format time

Controllers
Dynamic model selection
Including first item from a sorted desc table
Using from_unixtime on epoch dates
Custom SQL Query Examples

Rack
Integrated NTLM/Kerberos Authentication
Pass-through Authentication w/ NTLM

ActionMailer
Broken links in emails

Rails General
Add, Subtract, Multiply, and Divide
Calculate number of weekdays for date range
Date->Epoch & Epoch->Date
Calculate past/present payroll dates
Extract first letter of each word
Hash of hashes assignment
Using: variable as hash index

jQuery
jQuery accordion MongoDB

Rails Framework Examples

Apotomo Widget Using Erb

MySQL
Converting Julian Dates to Epoch

d3 Charting
Configuration to Work with Rails Apps
A simple bar chart example

Other
Setup VPN on iMac
SSH Key Generation

More to come

 

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.zip

Installing 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.

 

(C) Copyright Birds-Eye.Net, All rights reserved.
It is against the law to reproduce this content or any portion of it in any form without the explicit written permission of Birds-Eye Network Services, LLC. Federal copyright law (17 USC 504) makes it illegal, punishable with fines up to $100,000 per violation plus attorney's fees.