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
|
||||
coverage/
|
||||
Gemfile.lock
|
||||
.sass-cache/
|
||||
|
1
Gemfile
1
Gemfile
@ -12,6 +12,7 @@ group :development, :test do
|
||||
gem "simplecov", :require => false
|
||||
|
||||
gem "generator_spec"
|
||||
gem "formtastic"
|
||||
end
|
||||
|
||||
# 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
|
||||
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.
|
||||
|
||||
I have a sample project where I demonstrate the use of cocoon with formtastic.
|
||||
@ -37,6 +39,13 @@ asset_pipeline
|
||||
//= 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
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
|
||||
Inside our `projects/_form` partial we then write:
|
||||
|
@ -7,6 +7,9 @@
|
||||
content.replace(reg_exp, with_str);
|
||||
}
|
||||
|
||||
$.fn.parentSiblings = function(selector) {
|
||||
return $(this).parent().siblings(selector);
|
||||
}
|
||||
|
||||
$('.add_fields').live('click', function(e) {
|
||||
e.preventDefault();
|
||||
@ -67,7 +70,8 @@
|
||||
|
||||
var timeout = trigger_node.data('remove-timeout') || 0;
|
||||
|
||||
setTimeout(function() {
|
||||
setTimeout(
|
||||
function() {
|
||||
if ($this.hasClass('dynamic')) {
|
||||
$this.closest(".nested-fields").remove();
|
||||
} 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
|
||||
initializer "cocoon.initialize" do |app|
|
||||
ActionView::Base.send :include, Cocoon::ViewHelpers
|
||||
|
||||
if Object.const_defined?("Formtastic") and Formtastic.const_defined?("Inputs")
|
||||
require 'cocoon/formtastic/cocoon_input'
|
||||
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)
|
||||
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)
|
||||
|
||||
f.send(method_name, association, new_object, {:child_index => "new_#{association}"}.merge(render_options)) do |builder|
|
||||
partial_options = {:f => builder, :dynamic => true}.merge(locals)
|
||||
render(partial, partial_options)
|
||||
@ -99,6 +100,14 @@ module Cocoon
|
||||
partial ? partial : association.to_s.singularize + "_fields"
|
||||
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
|
||||
|
||||
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