Compare commits
16 Commits
master
...
aepstein/p
Author | SHA1 | Date | |
---|---|---|---|
|
00643beceb | ||
|
6d85276a4e | ||
|
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/
|
||||||
|
1
Gemfile
1
Gemfile
@ -12,6 +12,7 @@ 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+)
|
||||||
|
@ -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:
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
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) {
|
$('.add_fields').live('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -67,7 +70,8 @@
|
|||||||
|
|
||||||
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 {
|
||||||
|
88
app/assets/javascripts/cocoon/ordered.js
Normal file
88
app/assets/javascripts/cocoon/ordered.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
//= require jquery-ui
|
||||||
|
//
|
||||||
|
(function($) {
|
||||||
|
$.cocoon = {
|
||||||
|
ordered: {
|
||||||
|
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) {
|
||||||
|
console.log(element)
|
||||||
|
console.log($(element).data('fieldSearch'))
|
||||||
|
console.log($(element).find($(element).data('fieldSearch')))
|
||||||
|
|
||||||
|
$(element).find($(element).data('fieldSearch')).each(function(index, element) {
|
||||||
|
$(element).val(index + 1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setup: function() {
|
||||||
|
$('li[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)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('cocoon:after-insert', function() { $.cocoon.ordered.setup(); });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,10 @@ module Cocoon
|
|||||||
# 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
|
||||||
|
52
lib/cocoon/formtastic/cocoon_input.rb
Normal file
52
lib/cocoon/formtastic/cocoon_input.rb
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
require 'formtastic'
|
||||||
|
|
||||||
|
class CocoonInput
|
||||||
|
include ::Formtastic::Inputs::Base
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
output = label_html << wrapped_semantic_fields << links
|
||||||
|
|
||||||
|
template.content_tag(:li, output.html_safe, wrapper_html_options)
|
||||||
|
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, input_html_options
|
||||||
|
end
|
||||||
|
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)
|
||||||
@ -99,6 +100,14 @@ module Cocoon
|
|||||||
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)
|
||||||
|
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