Compare commits

..

16 Commits

Author SHA1 Message Date
Ari Epstein 00643beceb on stop set first item position at 1, not 0 2012-12-08 18:17:26 -05:00
John Bintz 6d85276a4e add cocoon wrapper 2012-11-27 10:17:53 -05:00
John Bintz 83beb92d4a refactor and make i18n nicer 2012-11-01 13:19:36 -04:00
John Bintz 09fe15c446 fix broken test 2012-10-26 17:59:48 -04:00
John Bintz a1715f049b more work to make nested sortable forms work, can now sort forms within forms 2012-10-26 17:59:07 -04:00
John Bintz 511184a267 fix merge problem 2012-10-25 15:27:25 -04:00
John Bintz b8f373fd5e add the most basic sanity tests for formtastic input 2012-10-25 15:17:47 -04:00
John Bintz 042fcc9976 fix dumb bug 2012-10-25 15:02:00 -04:00
John Bintz 53074502b1 merge in master 2012-10-25 14:59:39 -04:00
John Bintz 6503f8eafa merge 2012-10-25 14:51:07 -04:00
John Bintz bfe72bd29f add support for sorting nested forms 2012-10-25 14:49:55 -04:00
John Bintz 215b31cb59 Merge branch 'formtastic_input' of github.com:johnbintz/cocoon into formtastic_input 2012-10-21 13:46:00 -04:00
John Bintz 663a9f344c some fixes 2012-10-21 13:12:29 -04:00
John Bintz f584e181d2 ensure important 2012-10-18 14:02:10 -04:00
John Bintz 3f89fff364 add stylesheet for active admin integration 2012-10-17 11:21:19 -04:00
John Bintz 78b54a1002 hack in formtastic input 2012-10-17 10:09:53 -04:00
11 changed files with 321 additions and 4 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ spec/dummy/tmp/
.idea
coverage/
Gemfile.lock
.sass-cache/

View File

@ -12,8 +12,9 @@ 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+)
# gem 'ruby-debug'
# gem 'ruby-debug19'
# gem 'ruby-debug19'

View File

@ -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:

View File

@ -39,4 +39,4 @@ begin
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end
end

View File

@ -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 {

View 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);

View 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;
}
}
}

View File

@ -10,7 +10,11 @@ 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
end
end

View 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

View File

@ -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)

View 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