Skip to content

Commit

Permalink
Added support for Rails 2.3 nested attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
patshaughnessy committed Jun 13, 2009
1 parent 8c59998 commit a21eec5
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 64 deletions.
38 changes: 33 additions & 5 deletions README
Expand Up @@ -11,24 +11,52 @@ See: http://patshaughnessy.net/repeated_auto_complete for details.
Repeated autocomplete text fields:
==================================

A "text_field_with_auto_complete" method is made available to form_for and fields_for that:
- Insures unique id's for <input> and <div> tags
A "text_field_with_auto_complete" method is made available to the form builder
yielded by form_for and fields_for that:
- Insures unique id's for <input> and <div> tags by inserting unique integers into their
id attributes
- Uses the object name from the surrounding call to form_for or fields_for so attribute
mass assignment will work as usual
- Works with the same server side controller method, auto_complete_for, as usual
- Support nested attributes in Rails 2.3

Example:
form.text_field_with_auto_complete works the same as the original text_field_with_auto_complete macro,
except it does not take the object as a parameter.

Example with nested attributes using Rails 2.3 or later:

class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks, :allow_destroy => true
end

<% form_for @project do |project_form| %>
<p>
<%= project_form.label :name, "Project:" %>
<%= project_form.text_field_with_auto_complete :name, {}, {:method => :get } %>
</p>
<% project_form.fields_for :tasks do |task_form| %>
<p>
<%= task_form.label :name, "Task:" %>
<%= task_form.text_field_with_auto_complete :name, {}, { :method => :get, :skip_style => true } %>
</p>
<% end %>
<% end %>


Rails 2.2 and earlier example:

<% for person in @group.people %>
<% auto_complete_fields_for "group[person_attributes][]", person do |person_form| %>
<% fields_for "group[person_attributes][]", person do |person_form| %>
<p>
Person <%= person_form.label :name %><br/>
<%= person_form.text_field_with_auto_complete :person, :name, {}, {:method => :get } %>
<%= person_form.text_field_with_auto_complete :name, {}, {:method => :get } %>
</p>
<% end %>
<% end %>



Named scopes with auto_complete_for:
====================================

Expand Down
2 changes: 1 addition & 1 deletion init.rb
@@ -1,3 +1,3 @@
ActionController::Base.send :include, AutoComplete
ActionController::Base.helper AutoCompleteMacrosHelper
ActionView::Base.send :include, AutoCompleteFormHelper
ActionView::Helpers::FormBuilder.send :include, AutoCompleteFormBuilder
45 changes: 26 additions & 19 deletions lib/auto_complete_form_helper.rb
@@ -1,29 +1,36 @@
module AutoCompleteFormHelper
[:form_for, :fields_for, :form_remote_for, :remote_form_for].each do |meth|
src = <<-end_src
def auto_complete_#{meth}(object_name, *args, &proc)
options = args.last.is_a?(Hash) ? args.pop : {}
options.update(:builder => AutoCompleteFormBuilder)
#{meth}(object_name, *(args << options), &proc)
end
end_src
module_eval src, __FILE__, __LINE__
module AutoCompleteFormBuilder

def class_name
"#{@object.class.to_s.underscore}"
end

def sanitized_object_name
@object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
end
end

class AutoCompleteFormBuilder < ActionView::Helpers::FormBuilder
def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})
unique_object_name = "#{object}_#{Object.new.object_id.abs}"
completion_options_for_original_name = { :url => { :action => "auto_complete_for_#{object}_#{method}" },
:param_name => "#{object}[#{method}]"
}.update(completion_options)
def is_used_as_nested_attribute?
/\[#{class_name.pluralize}_attributes\]\[[0-9]+\]/.match @object_name
end

def text_field_with_auto_complete(method, tag_options = {}, completion_options = {})
if is_used_as_nested_attribute?
unique_object_name = sanitized_object_name
elsif options[:index]
unique_object_name = "#{class_name}_#{@options[:index]}"
else
unique_object_name = "#{class_name}_#{Object.new.object_id.abs}"
end
completion_options_for_class_name = {
:url => { :action => "auto_complete_for_#{class_name}_#{method}" },
:param_name => "#{class_name}[#{method}]"
}.update(completion_options)
@template.auto_complete_field_with_style_and_script(unique_object_name,
method,
tag_options,
completion_options_for_original_name
completion_options_for_class_name
) do
text_field(method, { :id => "#{unique_object_name}_#{method}" }.update(tag_options))
end
end

end
end
53 changes: 29 additions & 24 deletions test/auto_complete_form_helper_test.rb
@@ -1,5 +1,15 @@
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../test/test_helper'))

class Person
attr_accessor :name, :id
def initialize name, id = nil
@name, @id = name, id
end
def to_param
id.to_s
end
end

class AutoCompleteFormHelperTest < Test::Unit::TestCase

include AutoCompleteMacrosHelper
Expand All @@ -11,13 +21,8 @@ class AutoCompleteFormHelperTest < Test::Unit::TestCase

def setup

person_class = Class.new(Struct.new(:name, :id)) do
def to_param
id.to_s
end
end
@existing_person = person_class.new "Existing Person", 1234
@person = person_class.new "New Person"
@existing_person = Person.new "Existing Person", 1234
@person = Person.new "New Person"

controller_class = Class.new do
def url_for(options)
Expand All @@ -34,9 +39,9 @@ def test_two_auto_complete_fields_have_different_ids
id_attribute_pattern = /id=\"[^\"]*\"/i
_erbout = ''
_erbout2 = ''
auto_complete_fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
_erbout2.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
_erbout2.concat f.text_field_with_auto_complete(:name)
end
assert_equal [], _erbout.scan(id_attribute_pattern) & _erbout2.scan(id_attribute_pattern)
end
Expand All @@ -46,50 +51,50 @@ def test_compare_macro_to_fields_for
text_field_with_auto_complete :person, :name

_erbout = ''
auto_complete_fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
end

assert_dom_equal standard_auto_complete_html,
assert_equal standard_auto_complete_html,
_erbout.gsub(/group\[person_attributes\]\[\]/, 'person').gsub(/person_[0-9]+_name/, 'person_name').gsub(/paramName:'person\[name\]'/, '')
end

def test_ajax_url
_erbout = ''
auto_complete_fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
end
assert _erbout.index('http://www.example.com/auto_complete_for_person_name')
end

def test_ajax_param
_erbout = ''
auto_complete_fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
end
assert _erbout.index("{paramName:'person[name]'}")
end

def test_object_value
_erbout = ''
auto_complete_fields_for('group[person_attributes][]', @existing_person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @existing_person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
end
assert _erbout.index('value="Existing Person"')
end

def test_auto_index_value_for_existing_record
_erbout = ''
auto_complete_fields_for('group[person_attributes][]', @existing_person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @existing_person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
end
assert _erbout.index("[1234]")
end

def test_auto_index_value_for_new_record
_erbout = ''
auto_complete_fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:person, :name)
fields_for('group[person_attributes][]', @person) do |f|
_erbout.concat f.text_field_with_auto_complete(:name)
end
assert _erbout.index("[]")
end
Expand Down
132 changes: 132 additions & 0 deletions test/auto_complete_nested_attributes_test.rb
@@ -0,0 +1,132 @@
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../test/test_helper'))

# Note: These tests require nested attributes (Rails 2.3 or greater).

config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.establish_connection(config['test'])

Object.const_set("ParentTestModel", Class.new(ActiveRecord::Base))
Object.const_set("ChildTestModel", Class.new(ActiveRecord::Base))

ActiveRecord::Base.connection.create_table :parent_test_models, :force => true do |table|
table.column :name, :string
end

ActiveRecord::Base.connection.create_table :child_test_models, :force => true do |table|
table.column :name, :string
end

ParentTestModel.class_eval do
has_many :child_test_models
accepts_nested_attributes_for :child_test_models, :allow_destroy => true
end

ChildTestModel.class_eval do
belongs_to :parent_test_model
end

class AutoCompleteNestedAttributesTest < Test::Unit::TestCase

include AutoCompleteMacrosHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::FormHelper
include AutoCompleteFormHelper

def setup

@parent = ParentTestModel.new :name => 'Name of existing parent model'
3.times do |i|
@parent.child_test_models.build :name => "Name of child model #{i}"
end

controller_class = Class.new do
def url_for(options)
url = "http://www.example.com/"
url << options[:action].to_s if options and options[:action]
url
end
end
@controller = controller_class.new

end

def test_nested_attributes_all_have_different_ids
id_attribute_pattern = /id=\"[^\"]*\"/i
_erbout = []
fields_for @parent do |parent_form|
parent_form.fields_for :child_test_models do |child_form|
_erbout << child_form.text_field_with_auto_complete(:name, {}, { :method => :get })
end
end
assert_equal [], _erbout[0].scan(id_attribute_pattern) & _erbout[1].scan(id_attribute_pattern)
assert_equal [], _erbout[0].scan(id_attribute_pattern) & _erbout[2].scan(id_attribute_pattern)
assert_equal [], _erbout[1].scan(id_attribute_pattern) & _erbout[2].scan(id_attribute_pattern)
end

def test_ajax_url
_erbout = ''
fields_for @parent do |parent_form|
parent_form.fields_for :child_test_models do |child_form|
_erbout.concat child_form.text_field_with_auto_complete(:name, {}, { :method => :get })
end
end
assert _erbout.index('http://www.example.com/auto_complete_for_child_test_model_name')
end

def test_ajax_param
_erbout = ''
fields_for @parent do |parent_form|
parent_form.fields_for :child_test_models do |child_form|
_erbout.concat child_form.text_field_with_auto_complete(:name, {}, { :method => :get })
end
end
assert _erbout.index("paramName:'child_test_model[name]'")
end

def test_object_value
_erbout = ''
fields_for @parent do |parent_form|
parent_form.fields_for :child_test_models do |child_form|
_erbout.concat child_form.text_field_with_auto_complete(:name, {}, { :method => :get })
end
end
3.times do |i|
assert _erbout.index("value=\"Name of child model #{i}\"")
end
end

def test_sanitized_object_name
fields_for @parent do |parent_form|
assert_equal 'parent_test_model',
parent_form.sanitized_object_name
parent_form.fields_for :child_test_models, { :child_index => 1234 } do |child_form|
assert_equal 'parent_test_model_child_test_models_attributes_1234',
child_form.sanitized_object_name
end

end
end

def test_is_used_as_nested_attribute
fields_for @parent do |parent_form|
assert !parent_form.is_used_as_nested_attribute?
parent_form.fields_for :child_test_models do |child_form|
assert child_form.is_used_as_nested_attribute?
end
end
fields_for 'parent[child_test_models_attributes][]', @parent do |rails_2_2_form|
assert !rails_2_2_form.is_used_as_nested_attribute?
end
end

def test_index_option
_erbout = ''
fields_for @parent, { :index => 5678 } do |parent_form|
_erbout.concat parent_form.text_field_with_auto_complete(:name, {}, { :method => :get })
assert _erbout.index('parent_test_model_5678_name')
end
end


end

0 comments on commit a21eec5

Please sign in to comment.