Compare commits
37 Commits
master
...
formtastic
Author | SHA1 | Date | |
---|---|---|---|
|
2e2a746fe8 | ||
|
7b8cd65a55 | ||
|
7f6d0c0eb3 | ||
|
d4f59eb5eb | ||
|
de1acdf930 | ||
|
aac7105f04 | ||
|
c40d9becc3 | ||
|
bb6ffdbcdb | ||
|
e4c3fe8023 | ||
|
33f5df9d4c | ||
|
242d89a3b0 | ||
|
996ccd55f0 | ||
|
d9e94ab3ed | ||
|
b40e73734c | ||
|
7ce9fe91f3 | ||
|
0484ccbab5 | ||
|
003229a558 | ||
|
6d85276a4e | ||
|
a7a9147ad4 | ||
|
3c0c82ce90 | ||
|
ed8154d2fa | ||
|
1c82d74acf | ||
|
2350ae3ba2 | ||
|
83beb92d4a | ||
|
09fe15c446 | ||
|
a1715f049b | ||
|
511184a267 | ||
|
b8f373fd5e | ||
|
042fcc9976 | ||
|
53074502b1 | ||
|
6503f8eafa | ||
|
bfe72bd29f | ||
|
215b31cb59 | ||
|
663a9f344c | ||
|
f584e181d2 | ||
|
3f89fff364 | ||
|
78b54a1002 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ spec/dummy/tmp/
|
|||||||
.idea
|
.idea
|
||||||
coverage/
|
coverage/
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
|
.sass-cache/
|
||||||
|
6
Gemfile
6
Gemfile
@ -1,5 +1,8 @@
|
|||||||
source "http://rubygems.org"
|
source "http://rubygems.org"
|
||||||
|
|
||||||
|
grop :assets do
|
||||||
|
gem 'jquery-ui-rails'
|
||||||
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem "rails", ">=3.0.0"
|
gem "rails", ">=3.0.0"
|
||||||
@ -12,8 +15,9 @@ group :development, :test do
|
|||||||
gem "simplecov", :require => false
|
gem "simplecov", :require => false
|
||||||
|
|
||||||
gem "generator_spec"
|
gem "generator_spec"
|
||||||
|
gem "formtastic"
|
||||||
end
|
end
|
||||||
|
|
||||||
# To use debugger (ruby-debug for Ruby 1.8.7+, ruby-debug19 for Ruby 1.9.2+)
|
# To use debugger (ruby-debug for Ruby 1.8.7+, ruby-debug19 for Ruby 1.9.2+)
|
||||||
# gem 'ruby-debug'
|
# gem 'ruby-debug'
|
||||||
# gem 'ruby-debug19'
|
# gem 'ruby-debug19'
|
||||||
|
10
History.md
10
History.md
@ -1,5 +1,15 @@
|
|||||||
# Change History / Release Notes
|
# Change History / Release Notes
|
||||||
|
|
||||||
|
## Version 1.1.2
|
||||||
|
|
||||||
|
* pull #118 (thanks @ahmozkya): remove the deprecated `.live` function, and use `.on` instead.
|
||||||
|
Note: at least jquery 1.7 is required now!
|
||||||
|
|
||||||
|
## Version 1.1.1
|
||||||
|
|
||||||
|
* added the to be added/deleted element to the event, this allows to add animations/actions onto them
|
||||||
|
* added extra option :wrap_object, allowing to use Decorators instead of the association object
|
||||||
|
* added an option :force_non_association_create, that will allow to use `link_to_add_association` inside the fields-partial
|
||||||
|
|
||||||
## Version 1.1.0
|
## Version 1.1.0
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# cocoon
|
# cocoon
|
||||||
|
|
||||||
[![Build Status](http://travis-ci.org/nathanvda/cocoon.png)](http://travis-ci.org/nathanvda/cocoon)
|
[![Build Status](https://travis-ci.org/nathanvda/cocoon.png)](https://travis-ci.org/nathanvda/cocoon)
|
||||||
|
|
||||||
cocoon is a Rails3 gem to allow easier handling of nested forms.
|
cocoon is a Rails3 gem to allow easier handling of nested forms.
|
||||||
|
|
||||||
@ -16,6 +16,8 @@ This project is not related to [Apache Cocoon](http://cocoon.apache.org/)
|
|||||||
This gem uses jQuery, it is most useful to use this gem in a rails3
|
This gem uses jQuery, it is most useful to use this gem in a rails3
|
||||||
project where you are already using jQuery.
|
project where you are already using jQuery.
|
||||||
|
|
||||||
|
Sortable form support requires jQuery UI.
|
||||||
|
|
||||||
Furthermore i would advice you to use either formtastic or simple_form.
|
Furthermore i would advice you to use either formtastic or simple_form.
|
||||||
|
|
||||||
I have a sample project where I demonstrate the use of cocoon with formtastic.
|
I have a sample project where I demonstrate the use of cocoon with formtastic.
|
||||||
@ -37,6 +39,13 @@ asset_pipeline
|
|||||||
//= require cocoon
|
//= require cocoon
|
||||||
````
|
````
|
||||||
|
|
||||||
|
If you also want to be able to sort nested forms, ordering them on a particular field, add `cocoon/ordered`:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
//= require cocoon
|
||||||
|
//= require cocoon/ordered
|
||||||
|
```
|
||||||
|
|
||||||
### Rails 3.0.x
|
### Rails 3.0.x
|
||||||
|
|
||||||
If you are using Rails 3.0.x, you need to run the installation task (since rails 3.1 this is no longer needed):
|
If you are using Rails 3.0.x, you need to run the installation task (since rails 3.1 this is no longer needed):
|
||||||
@ -125,6 +134,53 @@ That is all there is to it!
|
|||||||
|
|
||||||
There is an example project on github implementing it called [cocoon_formtastic_demo](https://github.com/nathanvda/cocoon_formtastic_demo).
|
There is an example project on github implementing it called [cocoon_formtastic_demo](https://github.com/nathanvda/cocoon_formtastic_demo).
|
||||||
|
|
||||||
|
Or, you can use the Formtastic `cocoon` field type to wrap up much of the boilerplate of the wrapper and
|
||||||
|
add association button:
|
||||||
|
|
||||||
|
``` haml
|
||||||
|
= f.inputs do
|
||||||
|
= f.input :name
|
||||||
|
= f.input :description
|
||||||
|
%h3 Tasks
|
||||||
|
#tasks
|
||||||
|
= f.input :tasks, :as => :cocoon
|
||||||
|
= f.actions do
|
||||||
|
= f.action :submit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sortable forms
|
||||||
|
|
||||||
|
Say you have a set of nested models that are ordered arbitrarily:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
class Task < ActiveRecord::Base
|
||||||
|
belongs_to :project
|
||||||
|
|
||||||
|
default_scope :order => 'order ASC'
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
You want users to be able to sort those
|
||||||
|
models via the UI. You can do this by including `cocoon/ordered` and specifying the sort field in the Formtastic
|
||||||
|
input call:
|
||||||
|
|
||||||
|
``` haml
|
||||||
|
= f.input :tasks, :as => :cocoon, :ordered_by => :order
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the order field as a hidden field in the nested form:
|
||||||
|
|
||||||
|
``` haml
|
||||||
|
.nested-fields
|
||||||
|
= f.inputs do
|
||||||
|
= f.input :description
|
||||||
|
= f.input :done, :as => :boolean
|
||||||
|
= f.input :order, :as => :hidden
|
||||||
|
= link_to_remove_association "remove task", f
|
||||||
|
```
|
||||||
|
|
||||||
|
The order field will now be filled in correctly when new models are added and when the models are sorted.
|
||||||
|
|
||||||
### Using simple_form
|
### Using simple_form
|
||||||
|
|
||||||
Inside our `projects/_form` partial we then write:
|
Inside our `projects/_form` partial we then write:
|
||||||
@ -217,6 +273,7 @@ It takes four parameters:
|
|||||||
- `render_options` : options passed through to the form-builder function (e.g. `simple_fields_for`, `semantic_fields_for` or `fields_for`).
|
- `render_options` : options passed through to the form-builder function (e.g. `simple_fields_for`, `semantic_fields_for` or `fields_for`).
|
||||||
If it contains a `:locals` option containing a hash, that is handed to the partial.
|
If it contains a `:locals` option containing a hash, that is handed to the partial.
|
||||||
- `wrap_object` : a proc that will allow to wrap your object, especially useful if you are using decorators (e.g. draper). See example lower.
|
- `wrap_object` : a proc that will allow to wrap your object, especially useful if you are using decorators (e.g. draper). See example lower.
|
||||||
|
- `force_non_association_create`: if true, it will _not_ create the new object using the association (see lower)
|
||||||
|
|
||||||
Optionally you could also leave out the name and supply a block that is captured to give the name (if you want to do something more complicated).
|
Optionally you could also leave out the name and supply a block that is captured to give the name (if you want to do something more complicated).
|
||||||
|
|
||||||
@ -288,6 +345,22 @@ link_to_add_association('add something', @form_obj, :comments,
|
|||||||
:wrap_object => Proc.new { |comment| comment.name = current_user.name; comment })
|
:wrap_object => Proc.new { |comment| comment.name = current_user.name; comment })
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### :force_non_association_create
|
||||||
|
|
||||||
|
In normal cases we create a new nested object using the association relation itself. This is the cleanest way to create
|
||||||
|
a new nested object. But this has a side-effect: for each call of `link_to_add_association` a new element is added to the association.
|
||||||
|
|
||||||
|
In most cases this is not a problem, but if you want to render a `link_to_add_association` for each nested element this will result
|
||||||
|
in an infinite loop.
|
||||||
|
|
||||||
|
To resolve this, specify that `:force_non_association_create` should be `true`, as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
link_to_add_association('add something', @form_obj, :comments, :force_non_association_create => true)
|
||||||
|
```
|
||||||
|
|
||||||
|
By default `:force_non_association_create` is `false`.
|
||||||
|
|
||||||
> A cleaner option would be to call a function that performs this initialisation and returns `self` at the end.
|
> A cleaner option would be to call a function that performs this initialisation and returns `self` at the end.
|
||||||
|
|
||||||
### link_to_remove_association
|
### link_to_remove_association
|
||||||
|
2
Rakefile
2
Rakefile
@ -39,4 +39,4 @@ begin
|
|||||||
Jeweler::GemcutterTasks.new
|
Jeweler::GemcutterTasks.new
|
||||||
rescue LoadError
|
rescue LoadError
|
||||||
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
||||||
end
|
end
|
||||||
|
@ -7,8 +7,11 @@
|
|||||||
content.replace(reg_exp, with_str);
|
content.replace(reg_exp, with_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$.fn.parentSiblings = function(selector) {
|
||||||
|
return $(this).parent().siblings(selector);
|
||||||
|
}
|
||||||
|
|
||||||
$('.add_fields').live('click', function(e) {
|
$(document).on('click', '.add_fields', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var $this = $(this),
|
var $this = $(this),
|
||||||
assoc = $this.data('association'),
|
assoc = $this.data('association'),
|
||||||
@ -55,7 +58,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$('.remove_fields.dynamic, .remove_fields.existing').live('click', function(e) {
|
$(document).on('click', '.remove_fields.dynamic, .remove_fields.existing', function(e) {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
var node_to_delete = $this.closest(".nested-fields");
|
var node_to_delete = $this.closest(".nested-fields");
|
||||||
var trigger_node = node_to_delete.parent();
|
var trigger_node = node_to_delete.parent();
|
||||||
@ -64,10 +67,10 @@
|
|||||||
|
|
||||||
trigger_node.trigger('cocoon:before-remove', [node_to_delete]);
|
trigger_node.trigger('cocoon:before-remove', [node_to_delete]);
|
||||||
|
|
||||||
|
|
||||||
var timeout = trigger_node.data('remove-timeout') || 0;
|
var timeout = trigger_node.data('remove-timeout') || 0;
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(
|
||||||
|
function() {
|
||||||
if ($this.hasClass('dynamic')) {
|
if ($this.hasClass('dynamic')) {
|
||||||
$this.closest(".nested-fields").remove();
|
$this.closest(".nested-fields").remove();
|
||||||
} else {
|
} else {
|
||||||
|
87
app/assets/javascripts/cocoon/ordered.js
Normal file
87
app/assets/javascripts/cocoon/ordered.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
(function($) {
|
||||||
|
$.cocoon = {
|
||||||
|
ordered: {
|
||||||
|
isSetUp: false,
|
||||||
|
options: {
|
||||||
|
items: '> .nested-fields',
|
||||||
|
stop: function(e, ui) {
|
||||||
|
if (window.CKEDITOR) {
|
||||||
|
var editors = $(ui.item).data('cocoon_ckeditors');
|
||||||
|
var i, j;
|
||||||
|
|
||||||
|
for (i = 0, j = editors.length; i < j; ++i) {
|
||||||
|
var id = editors[i];
|
||||||
|
var editor = CKEDITOR.instances[id];
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy(true);
|
||||||
|
CKEDITOR.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
CKEDITOR.replace(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(ui.item).data('cocoon_ckeditors', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.cocoon.ordered._updateFields(this)
|
||||||
|
},
|
||||||
|
start: function(e, ui) {
|
||||||
|
if (window.CKEDITOR) {
|
||||||
|
var editors = [];
|
||||||
|
|
||||||
|
$(ui.item).find('textarea').each(function(index, element) {
|
||||||
|
var id = $(element).attr('id');
|
||||||
|
var editor = CKEDITOR.instances[id];
|
||||||
|
if (editor) {
|
||||||
|
editors.push(id);
|
||||||
|
|
||||||
|
editor.destroy();
|
||||||
|
CKEDITOR.remove(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(ui.item).data('cocoon_ckeditors', editors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_updateFields: function(element) {
|
||||||
|
$(element).find($(element).data('fieldSearch')).each(function(index, element) {
|
||||||
|
$(element).val(index);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setup: function() {
|
||||||
|
$('[data-ordered_by]').each(function(index, element) {
|
||||||
|
var field = $(element).data('ordered_by');
|
||||||
|
var fieldSelector = "[name*='[" + field + "]']"
|
||||||
|
var fieldGroupSelector = "> .forms > .nested-fields"
|
||||||
|
var orderFieldSelector = "> .nested-fields " + fieldSelector;
|
||||||
|
var fieldSearch = "> .forms " + orderFieldSelector;
|
||||||
|
|
||||||
|
$(element).find('.forms').data('fieldSearch', orderFieldSelector).sortable($.cocoon.ordered.options);
|
||||||
|
|
||||||
|
$(element).unbind('cocoon:after-insert').bind('cocoon:after-insert', function(e, node) {
|
||||||
|
var nextOrder = 0;
|
||||||
|
|
||||||
|
if ($(element).find(fieldGroupSelector).is(node)) {
|
||||||
|
$(element).find(fieldSearch).each(function() {
|
||||||
|
nextOrder = Math.max(nextOrder, Number($(this).val()));
|
||||||
|
});
|
||||||
|
|
||||||
|
$(node).find(fieldSelector).val(nextOrder + 1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!$.cocoon.ordered.isSetUp) {
|
||||||
|
$(document).on('cocoon:after-insert', function() { $.cocoon.ordered.setup(); });
|
||||||
|
$.cocoon.ordered.isSetUp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$(function() { $.cocoon.ordered.setup(); });
|
||||||
|
$(document).on('page:change', function() { $.cocoon.ordered.setup(); });
|
||||||
|
})(jQuery);
|
||||||
|
|
29
app/assets/stylesheets/cocoon/active_admin.css.scss
Normal file
29
app/assets/stylesheets/cocoon/active_admin.css.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
li.cocoon {
|
||||||
|
& > label {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: auto !important;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links a {
|
||||||
|
@extend .button;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forms.ui-sortable {
|
||||||
|
fieldset {
|
||||||
|
border-top: solid #777 12px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
Gem::Specification.new do |s|
|
Gem::Specification.new do |s|
|
||||||
s.name = "cocoon"
|
s.name = "cocoon"
|
||||||
s.version = "1.1.0"
|
s.version = "1.1.2"
|
||||||
|
|
||||||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
||||||
s.authors = ["Nathan Van der Auwera"]
|
s.authors = ["Nathan Van der Auwera"]
|
||||||
s.date = "2012-10-08"
|
s.date = "2013-01-21"
|
||||||
s.description = "Unobtrusive nested forms handling, using jQuery. Use this and discover cocoon-heaven."
|
s.description = "Unobtrusive nested forms handling, using jQuery. Use this and discover cocoon-heaven."
|
||||||
s.email = "nathan@dixis.com"
|
s.email = "nathan@dixis.com"
|
||||||
s.extra_rdoc_files = [
|
s.extra_rdoc_files = [
|
||||||
|
@ -4,13 +4,19 @@ module Cocoon
|
|||||||
class Engine < ::Rails::Engine
|
class Engine < ::Rails::Engine
|
||||||
|
|
||||||
config.before_initialize do
|
config.before_initialize do
|
||||||
config.action_view.javascript_expansions[:cocoon] = %w(cocoon)
|
if config.action_view.javascript_expansions
|
||||||
|
config.action_view.javascript_expansions[:cocoon] = %w(cocoon)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# configure our plugin on boot
|
# configure our plugin on boot
|
||||||
initializer "cocoon.initialize" do |app|
|
initializer "cocoon.initialize" do |app|
|
||||||
ActionView::Base.send :include, Cocoon::ViewHelpers
|
ActionView::Base.send :include, Cocoon::ViewHelpers
|
||||||
|
|
||||||
|
if Object.const_defined?("Formtastic") and Formtastic.const_defined?("Inputs")
|
||||||
|
require 'cocoon/formtastic/cocoon_input'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
62
lib/cocoon/formtastic/cocoon_input.rb
Normal file
62
lib/cocoon/formtastic/cocoon_input.rb
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
require 'formtastic'
|
||||||
|
|
||||||
|
class CocoonInput
|
||||||
|
include ::Formtastic::Inputs::Base
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
wrap_output(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
def wrap_output(output)
|
||||||
|
template.content_tag(:li, output.html_safe, wrapper_html_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def output
|
||||||
|
label_html << wrapped_semantic_fields << links
|
||||||
|
end
|
||||||
|
|
||||||
|
def wrapper_html_options
|
||||||
|
data = super.merge(:class => 'input cocoon')
|
||||||
|
if options[:ordered_by]
|
||||||
|
data['data-ordered_by'] = options[:ordered_by]
|
||||||
|
end
|
||||||
|
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
def semantic_fields_for
|
||||||
|
builder.semantic_fields_for(method) do |fields|
|
||||||
|
if fields.object
|
||||||
|
template.render :partial => "#{singular_method}_fields", :locals => { :f => fields }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def wrapped_semantic_fields
|
||||||
|
template.content_tag(:div, semantic_fields_for, :class => 'forms')
|
||||||
|
end
|
||||||
|
|
||||||
|
def links
|
||||||
|
template.content_tag(:div, :class => 'links') do
|
||||||
|
template.link_to_add_association template.t(".add_#{singular_method}"), builder, method, add_association_input_html_options
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_association_input_html_options
|
||||||
|
input_html_options
|
||||||
|
end
|
||||||
|
|
||||||
|
def input_html_options
|
||||||
|
super.merge(
|
||||||
|
'data-association-insertion-node' => '.forms',
|
||||||
|
'data-association-insertion-traversal' => 'parentSiblings',
|
||||||
|
'data-association-insertion-method' => 'append'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def singular_method
|
||||||
|
@singular_method ||= method.to_s.singularize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -35,6 +35,7 @@ module Cocoon
|
|||||||
partial = get_partial_path(custom_partial, association)
|
partial = get_partial_path(custom_partial, association)
|
||||||
locals = render_options.delete(:locals) || {}
|
locals = render_options.delete(:locals) || {}
|
||||||
method_name = f.respond_to?(:semantic_fields_for) ? :semantic_fields_for : (f.respond_to?(:simple_fields_for) ? :simple_fields_for : :fields_for)
|
method_name = f.respond_to?(:semantic_fields_for) ? :semantic_fields_for : (f.respond_to?(:simple_fields_for) ? :simple_fields_for : :fields_for)
|
||||||
|
|
||||||
f.send(method_name, association, new_object, {:child_index => "new_#{association}"}.merge(render_options)) do |builder|
|
f.send(method_name, association, new_object, {:child_index => "new_#{association}"}.merge(render_options)) do |builder|
|
||||||
partial_options = {:f => builder, :dynamic => true}.merge(locals)
|
partial_options = {:f => builder, :dynamic => true}.merge(locals)
|
||||||
render(partial, partial_options)
|
render(partial, partial_options)
|
||||||
@ -50,6 +51,8 @@ module Cocoon
|
|||||||
# - *:render_options* : options passed to `simple_fields_for, semantic_fields_for or fields_for`
|
# - *:render_options* : options passed to `simple_fields_for, semantic_fields_for or fields_for`
|
||||||
# - *:locals* : the locals hash in the :render_options is handed to the partial
|
# - *:locals* : the locals hash in the :render_options is handed to the partial
|
||||||
# - *:partial* : explicitly override the default partial name
|
# - *:partial* : explicitly override the default partial name
|
||||||
|
# - *:wrap_object : !!! document more here !!!
|
||||||
|
# - *!!!add some option to build in collection or not!!!*
|
||||||
# - *&block*: see <tt>link_to</tt>
|
# - *&block*: see <tt>link_to</tt>
|
||||||
|
|
||||||
def link_to_add_association(*args, &block)
|
def link_to_add_association(*args, &block)
|
||||||
@ -68,16 +71,14 @@ module Cocoon
|
|||||||
render_options ||= {}
|
render_options ||= {}
|
||||||
override_partial = html_options.delete(:partial)
|
override_partial = html_options.delete(:partial)
|
||||||
wrap_object = html_options.delete(:wrap_object)
|
wrap_object = html_options.delete(:wrap_object)
|
||||||
|
force_non_association_create = html_options.delete(:force_non_association_create) || false
|
||||||
|
|
||||||
html_options[:class] = [html_options[:class], "add_fields"].compact.join(' ')
|
html_options[:class] = [html_options[:class], "add_fields"].compact.join(' ')
|
||||||
html_options[:'data-association'] = association.to_s.singularize
|
html_options[:'data-association'] = association.to_s.singularize
|
||||||
html_options[:'data-associations'] = association.to_s.pluralize
|
html_options[:'data-associations'] = association.to_s.pluralize
|
||||||
|
|
||||||
if wrap_object.respond_to?(:call)
|
new_object = create_object(f, association, force_non_association_create)
|
||||||
new_object = wrap_object.call(create_object(f, association))
|
new_object = wrap_object.call(new_object) if wrap_object.respond_to?(:call)
|
||||||
else
|
|
||||||
new_object = create_object(f, association)
|
|
||||||
end
|
|
||||||
|
|
||||||
html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, render_options, override_partial)).html_safe
|
html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, render_options, override_partial)).html_safe
|
||||||
|
|
||||||
@ -89,16 +90,24 @@ module Cocoon
|
|||||||
# `` has_many :admin_comments, class_name: "Comment", conditions: { author: "Admin" }
|
# `` has_many :admin_comments, class_name: "Comment", conditions: { author: "Admin" }
|
||||||
# will create new Comment with author "Admin"
|
# will create new Comment with author "Admin"
|
||||||
|
|
||||||
def create_object(f, association)
|
def create_object(f, association, force_non_association_create=false)
|
||||||
assoc = f.object.class.reflect_on_association(association)
|
assoc = f.object.class.reflect_on_association(association)
|
||||||
|
|
||||||
assoc ? create_object_on_association(f, association, assoc) : create_object_on_non_association(f, association)
|
assoc ? create_object_on_association(f, association, assoc, force_non_association_create) : create_object_on_non_association(f, association)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_partial_path(partial, association)
|
def get_partial_path(partial, association)
|
||||||
partial ? partial : association.to_s.singularize + "_fields"
|
partial ? partial : association.to_s.singularize + "_fields"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cocoon_wrapper(form_builder, &block)
|
||||||
|
content_tag(:div, :class => 'nested-fields') do
|
||||||
|
content_tag(:fieldset) do
|
||||||
|
capture(&block) << link_to_remove_association(t('.remove'), form_builder)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_object_on_non_association(f, association)
|
def create_object_on_non_association(f, association)
|
||||||
@ -107,13 +116,12 @@ module Cocoon
|
|||||||
raise "Association #{association} doesn't exist on #{f.object.class}"
|
raise "Association #{association} doesn't exist on #{f.object.class}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_object_on_association(f, association, instance)
|
def create_object_on_association(f, association, instance, force_non_association_create)
|
||||||
if instance.class.name == "Mongoid::Relations::Metadata"
|
if instance.class.name == "Mongoid::Relations::Metadata" || force_non_association_create
|
||||||
conditions = instance.respond_to?(:conditions) ? instance.conditions.flatten : []
|
create_object_with_conditions(instance)
|
||||||
instance.klass.new(*conditions)
|
|
||||||
else
|
else
|
||||||
# assume ActiveRecord or compatible
|
# assume ActiveRecord or compatible
|
||||||
if instance.collection?
|
if instance.collection?
|
||||||
f.object.send(association).build
|
f.object.send(association).build
|
||||||
else
|
else
|
||||||
f.object.send("build_#{association}")
|
f.object.send("build_#{association}")
|
||||||
@ -121,5 +129,10 @@ module Cocoon
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_object_with_conditions(instance)
|
||||||
|
conditions = instance.respond_to?(:conditions) ? instance.conditions.flatten : []
|
||||||
|
instance.klass.new(*conditions)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,11 +10,15 @@ describe Cocoon do
|
|||||||
it { should respond_to(:link_to_add_association) }
|
it { should respond_to(:link_to_add_association) }
|
||||||
it { should respond_to(:link_to_remove_association) }
|
it { should respond_to(:link_to_remove_association) }
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
@tester = TestClass.new
|
||||||
|
@post = Post.new
|
||||||
|
@form_obj = stub(:object => @post, :object_name => @post.class.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
context "link_to_add_association" do
|
context "link_to_add_association" do
|
||||||
before(:each) do
|
before(:each) do
|
||||||
@tester = TestClass.new
|
|
||||||
@post = Post.new
|
|
||||||
@form_obj = stub(:object => @post)
|
|
||||||
@tester.stub(:render_association).and_return('form<tag>')
|
@tester.stub(:render_association).and_return('form<tag>')
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -41,6 +45,25 @@ describe Cocoon do
|
|||||||
@tester.should_receive(:render_association).with(anything(), anything(), kind_of(CommentDecorator), anything(), anything()).and_return('partiallll')
|
@tester.should_receive(:render_association).with(anything(), anything(), kind_of(CommentDecorator), anything(), anything()).and_return('partiallll')
|
||||||
@tester.link_to_add_association('add something', @form_obj, :comments, :wrap_object => Proc.new {|comment| CommentDecorator.new(comment) })
|
@tester.link_to_add_association('add something', @form_obj, :comments, :wrap_object => Proc.new {|comment| CommentDecorator.new(comment) })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "force non association create" do
|
||||||
|
it "default it uses the association" do
|
||||||
|
@tester.should_receive(:create_object).with(anything, :comments , false)
|
||||||
|
result = @tester.link_to_add_association('add something', @form_obj, :comments)
|
||||||
|
result.to_s.should == '<a href="#" class="add_fields" data-association-insertion-template="form<tag>" data-association="comment" data-associations="comments">add something</a>'
|
||||||
|
end
|
||||||
|
it "specifying false is the same as default: create object on association" do
|
||||||
|
@tester.should_receive(:create_object).with(anything, :comments , false)
|
||||||
|
result = @tester.link_to_add_association('add something', @form_obj, :comments, :force_non_association_create => false)
|
||||||
|
result.to_s.should == '<a href="#" class="add_fields" data-association-insertion-template="form<tag>" data-association="comment" data-associations="comments">add something</a>'
|
||||||
|
end
|
||||||
|
it "specifying true will not create objects on association but using the conditions" do
|
||||||
|
@tester.should_receive(:create_object).with(anything, :comments , true)
|
||||||
|
result = @tester.link_to_add_association('add something', @form_obj, :comments, :force_non_association_create => true)
|
||||||
|
result.to_s.should == '<a href="#" class="add_fields" data-association-insertion-template="form<tag>" data-association="comment" data-associations="comments">add something</a>'
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with a block" do
|
context "with a block" do
|
||||||
@ -146,12 +169,6 @@ describe Cocoon do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "link_to_remove_association" do
|
context "link_to_remove_association" do
|
||||||
before(:each) do
|
|
||||||
@tester = TestClass.new
|
|
||||||
@post = Post.new
|
|
||||||
@form_obj = stub(:object => @post, :object_name => @post.class.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
context "without a block" do
|
context "without a block" do
|
||||||
it "accepts a name" do
|
it "accepts a name" do
|
||||||
result = @tester.link_to_remove_association('remove something', @form_obj)
|
result = @tester.link_to_remove_association('remove something', @form_obj)
|
||||||
@ -180,44 +197,50 @@ describe Cocoon do
|
|||||||
result.to_s.should == "<input id=\"Post__destroy\" name=\"Post[_destroy]\" type=\"hidden\" /><a href=\"#\" class=\"add_some_class remove_fields dynamic\" data-something=\"bla\">remove some long name</a>"
|
result.to_s.should == "<input id=\"Post__destroy\" name=\"Post[_destroy]\" type=\"hidden\" /><a href=\"#\" class=\"add_some_class remove_fields dynamic\" data-something=\"bla\">remove some long name</a>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "create_object" do
|
context "create_object" do
|
||||||
it "should create correct association with conditions" do
|
it "creates correct association with conditions" do
|
||||||
result = @tester.create_object(@form_obj, :admin_comments)
|
@tester.should_not_receive(:create_object_with_conditions)
|
||||||
result.author.should == "Admin"
|
result = @tester.create_object(@form_obj, :admin_comments)
|
||||||
end
|
result.author.should == "Admin"
|
||||||
|
|
||||||
it "should create correct association for belongs_to associations" do
|
|
||||||
result = @tester.create_object(stub(:object => Comment.new), :post)
|
|
||||||
result.should be_a Post
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should raise error if cannot reflect on association" do
|
|
||||||
expect { @tester.create_object(stub(:object => Comment.new), :not_existing) }.to raise_error /association/i
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should create an association if object responds to 'build_association' as singular" do
|
|
||||||
object = Comment.new
|
|
||||||
object.should_receive(:build_custom_item).and_return 'custom'
|
|
||||||
@tester.create_object(stub(:object => object), :custom_item).should == 'custom'
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should create an association if object responds to 'build_association' as plural" do
|
|
||||||
object = Comment.new
|
|
||||||
object.should_receive(:build_custom_item).and_return 'custom'
|
|
||||||
@tester.create_object(stub(:object => object), :custom_items).should == 'custom'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "get_partial_path" do
|
it "creates correct association for belongs_to associations" do
|
||||||
it "generates the default partial name if no partial given" do
|
result = @tester.create_object(stub(:object => Comment.new), :post)
|
||||||
result = @tester.get_partial_path(nil, :admin_comments)
|
result.should be_a Post
|
||||||
result.should == "admin_comment_fields"
|
end
|
||||||
end
|
|
||||||
it "uses the given partial name" do
|
it "raises an error if cannot reflect on association" do
|
||||||
result = @tester.get_partial_path("comment_fields", :admin_comments)
|
expect { @tester.create_object(stub(:object => Comment.new), :not_existing) }.to raise_error /association/i
|
||||||
result.should == "comment_fields"
|
end
|
||||||
end
|
|
||||||
|
it "creates an association if object responds to 'build_association' as singular" do
|
||||||
|
object = Comment.new
|
||||||
|
object.should_receive(:build_custom_item).and_return 'custom'
|
||||||
|
@tester.create_object(stub(:object => object), :custom_item).should == 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates an association if object responds to 'build_association' as plural" do
|
||||||
|
object = Comment.new
|
||||||
|
object.should_receive(:build_custom_item).and_return 'custom'
|
||||||
|
@tester.create_object(stub(:object => object), :custom_items).should == 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can create using only conditions not the association" do
|
||||||
|
@tester.should_receive(:create_object_with_conditions).and_return('flappie')
|
||||||
|
@tester.create_object(@form_obj, :comments, true).should == 'flappie'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "get_partial_path" do
|
||||||
|
it "generates the default partial name if no partial given" do
|
||||||
|
result = @tester.get_partial_path(nil, :admin_comments)
|
||||||
|
result.should == "admin_comment_fields"
|
||||||
|
end
|
||||||
|
it "uses the given partial name" do
|
||||||
|
result = @tester.get_partial_path("comment_fields", :admin_comments)
|
||||||
|
result.should == "comment_fields"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
73
spec/formtastic/cocoon_input_spec.rb
Normal file
73
spec/formtastic/cocoon_input_spec.rb
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
require 'spec_helper'
|
||||||
|
require 'cocoon/formtastic/cocoon_input'
|
||||||
|
|
||||||
|
describe CocoonInput do
|
||||||
|
let(:input) { CocoonInput.new(builder, template, object, object_name, method, options) }
|
||||||
|
let(:builder) { stub(:auto_index => false, :options => {}, :custom_namespace => nil, :all_fields_required_by_default => false) }
|
||||||
|
let(:template) { stub }
|
||||||
|
let(:object) { stub }
|
||||||
|
let(:object_name) { :object }
|
||||||
|
let(:method) { :nested }
|
||||||
|
let(:options) { {} }
|
||||||
|
|
||||||
|
describe '#wrapper_html_options' do
|
||||||
|
subject { input.wrapper_html_options }
|
||||||
|
|
||||||
|
context 'not ordered' do
|
||||||
|
it 'should not be ordered' do
|
||||||
|
subject.should_not have_key('data-ordered_by')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ordered' do
|
||||||
|
let(:field) { :field }
|
||||||
|
let(:options) { { :ordered_by => field } }
|
||||||
|
|
||||||
|
it 'should be ordered' do
|
||||||
|
subject['data-ordered_by'].should == field
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#links' do
|
||||||
|
subject { input.links }
|
||||||
|
|
||||||
|
before do
|
||||||
|
template.stub(:content_tag).and_yield
|
||||||
|
template.stub(:link_to_add_association)
|
||||||
|
template.stub(:t)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should generate the links holder' do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#semantic_fields_for' do
|
||||||
|
subject { input.semantic_fields_for }
|
||||||
|
|
||||||
|
before do
|
||||||
|
builder.stub(:semantic_fields_for)
|
||||||
|
template.stub(:render)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should pass through to semantic_fields_for on the builder' do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#to_html' do
|
||||||
|
subject { input.to_html }
|
||||||
|
|
||||||
|
before do
|
||||||
|
input.stub(:label_html).and_return('label')
|
||||||
|
input.stub(:wrapped_semantic_fields).and_return('fields')
|
||||||
|
input.stub(:links).and_return('links')
|
||||||
|
template.stub(:content_tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should concatenate the outputs and pass through to content_tag' do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user