Every repository with this icon (
Every repository with this icon (
| name | age | message | |
|---|---|---|---|
| |
.gitignore | Thu May 08 10:52:59 -0700 2008 | |
| |
README.rdoc | Tue Feb 10 20:09:07 -0800 2009 | |
| |
Rakefile | Tue Apr 15 15:20:40 -0700 2008 | |
| |
app/ | Thu May 15 00:10:41 -0700 2008 | |
| |
config/ | Thu May 08 10:56:08 -0700 2008 | |
| |
db/ | Thu May 08 10:30:41 -0700 2008 | |
| |
doc/ | Tue Apr 15 15:20:40 -0700 2008 | |
| |
log/ | Thu May 15 00:10:41 -0700 2008 | |
| |
public/ | Wed Apr 16 04:22:59 -0700 2008 | |
| |
script/ | Thu May 08 06:10:45 -0700 2008 | |
| |
selenium/ | Thu May 08 06:10:45 -0700 2008 | |
| |
spec/ | Thu May 08 06:10:45 -0700 2008 | |
| |
stories/ | Thu May 08 06:10:45 -0700 2008 | |
| |
test/ | Thu May 08 07:42:05 -0700 2008 | |
| |
vendor/ | Thu May 08 10:51:01 -0700 2008 |
THIS APP IS OLD AND NO LONGER APPLIES =
Please see the built-in nested attribute hashes in rails 2.3:
ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
Welcome to the Complex Forms Demo App
Installation
git clone git://github.com/zilkey/complex_forms_demo_app.git cd complex_forms_demo_app cp config/database.yml.example config/database.yml #=> update database.yml to match your settings rake db:create rake db:migrate rake db:dataset:load_fixtures DATASET=nasa script/server
Then go to the projects page and click "Edit". Try every combination of adding, updating and deleting, watch the params and the validations and check your database to see when things get committed.
The Goal
The ultimate goal is to collect a number of real-world examples of complex html forms that involve multiple models with various associations and then figure out what, if anything, can be abstracted to a plugin and generator to help make creating these forms easier.
So far the app just shows how to create a complex form that captures 3 kinds of associations using skinny controllers and fat models. The 3 kinds of associations it demos are:
- has_many
- has_many :through (with extra attributes on the join table)
- has_many :through (with checkboxes to select the associated models)
It also includes a reusable class that adds mark_for_deletion to active record models.
Requirements
A form that manages a model and its related model must:
- Handle validation of the main model as well as all of the associated models
- Create fully valid XHTML
- Have completely atomic saves:
- If you add a child item and there is a form error the child item needs to be redisplayed to the user, but not saved to the database
- If you update a child item and there is an error the child item must be correctly displayed to the user but not saved in the database
- If you remove a child item and there is a form error, the child must be hidden from view on the page but remain in the database
- On a successful save all new objects must be created, existing objects updated and removed object deleted
- Run updates in a transaction
- Handle both means of data entry:
- As subforms that can be added
- As a list of checkboxes that can be checked
- Handle all of the rails helpers such as check boxes, date fields and radio buttons
- Work the same every time a user clicks refresh or submits
Domain Description for this app
Project is the main object here, and is defined as:
class Project < ActiveRecord::Base
has_many :tasks
has_many :assignments
has_many :employees, :through => :assignments
has_many :categorizations
has_many :categories, :through => :categorizations
validates_presence_of :name
validates_associated :tasks, :assignments
after_update :save_tasks
after_update :save_assignments
after_update :save_categorizations
end
Task belongs to Project and requires a name:
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :name
end
Employee is joined to Project through an Assignment and requires both an employee and a title:
class Employee < ActiveRecord::Base
has_many :assignments
has_many :projects, :through => :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :project
belongs_to :employee
validates_presence_of :title
validates_presence_of :employee_id
end
The project-employee-assignment part of the domain requires additional fields on the join table.
Category is joined to Project through a Categorization:
class Category < ActiveRecord::Base
has_many :categorizations
has_many :projects, :through => :categorizations
validates_presence_of :name
end
class Categorization < ActiveRecord::Base
belongs_to :project
belongs_to :category
end
The project-categorization-category part of the domain is very similar to a has_and_belongs_to_many relationship. Handling validation
Instead of using the rails default error_messages_for helper, just use a partial.
Creating fully valid XHMTL
To output valid XHTML you must:
* Set an index on every child field
* Have the javascript that dynamically adds fields set the index correctly as well
This requires a complex interaction between the partial and the javascript code. The necessary parts are spread across multiple files:
public/javascripts/application.js contains a generic class that you can use for any subform, and add multiple subforms to a page:
var Subform = Class.create({
lineIndex: 1,
parentElement: "",
// rawHTML contains the html to add using the "add" link
// lineIndex should be the length of the original array
// parentElement is the id of the div that the subforms attach to
initialize: function(rawHTML, lineIndex, parentElement) {
this.rawHTML = rawHTML;
this.lineIndex = lineIndex;
this.parentElement = parentElement;
},
// parses the rawHTML and replaces all instances of the word
// INDEX with the line index
// So the HTML on that rails outputs will be INDEX, but when this
// is added to the dom it has the correct id
parsedHTML: function() {
return this.rawHTML.replace(/INDEX/g, this.lineIndex++);
},
// handles the inserting of the child form
add: function() {
new Insertion.Bottom($(this.parentElement), this.parsedHTML());
}
});
Each child form’s partial must instantiate the javascript - I use a content_for block and inject that code into the head of the document
<%- content_for :head do -%>
<script type="text/javascript" charset="utf-8">
//<![CDATA[
taskForm = new Subform('<%= escape_javascript(render(:partial => "task", :object => Task.new, :locals => {:index => "INDEX"})) %>',<%= @project.tasks.length %>,'tasks');
assignmentForm = new Subform('<%= escape_javascript(render(:partial => "assignment", :object => Assignment.new, :locals => {:index => "INDEX"})) %>',<%= @project.assignments.length %>,'assignments');
//]]>
</script>
<%- end -%>
If you are using content_for like I am, then the app/views/application.html.erb file must have a corresponding yield statement
Rails makes setting the index properly a breeze with it’s :index option and the partial_counter local variable in partials that are fed collections.
The index does not work with radio buttons (this may be fixed in edge) and the standard rails labels output invalid XHTML, so those have to be coded manually. Marking objects for deletion
This means being able to mark an object for deletion before actually deleting it. I’ve accomplished this in a site-wide way with config/initializers/marked_for_deletion.rb Performing the save in a transaction
Rails doesn’t include the after_update callbacks in it’s default transactions, so you have to do that yourself. It has 3 parts:
A method in the project model that performs the save in a transaction and re-raises the error:
class << self
def save(project,project_attributes = nil)
project.attributes = project_attributes unless project_attributes.nil?
updated = false
Project.transaction do
updated = project.save
end
updated
rescue Exception => e
throw e
end
end
Make sure the child objects raise errors when they are saved:
def save_assignments
assignments.each do |assignment|
assignment.save!
end
end
Update the controller code to call the correct method:
def create
@project = Project.new(params[:project])
respond_to do |format|
if Project.save(@project)
...
else
...
end
end
end
def update
@project = Project.find(params[:id])
params[:project][:existing_task_attributes] ||= {}
respond_to do |format|
if Project.save(@project,params[:project])
...
else
...
end
end
end







