cocoon 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Change History / Release Notes
2
2
 
3
+ ## Version 1.2.1
4
+
5
+ * added a `:form_name` parameter (fixes #153) which allows to use a self-chosen
6
+ parameter in the nested views. Up until now `f` was assumed (and enforced).
7
+ * improvement of creation of the objects on the association (thanks to Dirk von Grünigen). This
8
+ alleviates the need for the `:force_non_association_create` option in most cases.
9
+ That option is for now still kept.
10
+ * after validation errors, already deleted (but not saved) nested elements, will remain deleted
11
+ (e.g. the state is remembered, and they remain hidden, and will be correctly deleted on next
12
+ succesfull save) (fixes #136).
13
+
14
+ ## Version 1.2.0
15
+
16
+ * support for rails 4.0
17
+
3
18
  ## Version 1.1.2
4
19
 
5
20
  * pull #118 (thanks @ahmozkya): remove the deprecated `.live` function, and use `.on` instead.
@@ -1,6 +1,6 @@
1
1
  # cocoon
2
2
 
3
- [![Build Status](https://travis-ci.org/nathanvda/cocoon.png)](https://travis-ci.org/nathanvda/cocoon)
3
+ [![Build Status](https://travis-ci.org/nathanvda/cocoon.png?branch=master)](https://travis-ci.org/nathanvda/cocoon)
4
4
 
5
5
  Cocoon makes it easier to handle nested forms.
6
6
 
@@ -25,8 +25,9 @@ Inside your `Gemfile` add the following:
25
25
  gem "cocoon"
26
26
  ```
27
27
 
28
+ > Please note that for rails 4 you will need at least v1.2.0 or later.
28
29
 
29
- ### Rails 3.1+
30
+ ### Rails 3.1+/Rails 4
30
31
 
31
32
  Add the following to `application.js` so it compiles to the asset pipeline:
32
33
 
@@ -79,6 +80,24 @@ Now we want a project form where we can add and remove tasks dynamically.
79
80
  To do this, we need the fields for a new or existing `task` to be defined in a partial
80
81
  named `_task_fields.html`.
81
82
 
83
+ ### Strong Parameters Gotcha
84
+
85
+ To destroy nested models, rails uses a virtual attribute called `_destroy`.
86
+ When `_destroy` is set, the nested model will be deleted.
87
+
88
+ When using strong parameters (default in rails 4), you need to explicitly
89
+ add `:_destroy` to the list of permitted parameters.
90
+
91
+ E.g. in your `ProjectsController`:
92
+
93
+ ```ruby
94
+ def project_params
95
+ params.require(:project).permit(:name, :description, tasks_attributes: [:id, :description, :done, :_destroy])
96
+ end
97
+ ```
98
+
99
+
100
+
82
101
  ## Examples
83
102
 
84
103
  ### Formtastic
@@ -202,6 +221,7 @@ This should be called within the form builder.
202
221
  If it contains a `:locals` option containing a hash, that is handed to the partial.
203
222
  - `wrap_object` : a proc that will allow to wrap your object, especially useful if you are using decorators (e.g. draper). See example lower.
204
223
  - `force_non_association_create`: if true, it will _not_ create the new object using the association (see lower)
224
+ - `form_name` : the name of the form parameter in your nested partial. By default this is `f`.
205
225
 
206
226
  Optionally, you can omit the name and supply a block that is captured to render the link body (if you want to do something more complicated).
207
227
 
@@ -281,12 +301,15 @@ E.g.
281
301
  #### :force_non_association_create
282
302
 
283
303
  In normal cases we create a new nested object using the association relation itself. This is the cleanest way to create
284
- 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.
304
+ a new nested object.
305
+
306
+ This used to have a side-effect: for each call of `link_to_add_association` a new element was added to the association.
307
+ This is no longer the case.
285
308
 
286
- 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
287
- in an infinite loop.
309
+ For backward compatibility we keep this option for now. Or if for some specific reason you would
310
+ really need an object to be _not_ created on the association.
288
311
 
289
- To resolve this, specify that `:force_non_association_create` should be `true`:
312
+ Example use:
290
313
 
291
314
  ```haml
292
315
  = link_to_add_association('add something', @form_obj, :comments,
@@ -295,8 +318,6 @@ To resolve this, specify that `:force_non_association_create` should be `true`:
295
318
 
296
319
  By default `:force_non_association_create` is `false`.
297
320
 
298
- > A cleaner option would be to call a function that performs this initialisation and returns `self` at the end.
299
-
300
321
  ### link_to_remove_association
301
322
 
302
323
  This function will add a link to your markup that, when clicked, dynamically removes the surrounding partial form.
@@ -323,7 +344,7 @@ On insertion or removal the following events are triggered:
323
344
  To listen to the events in your JavaScript:
324
345
 
325
346
  ```javascript
326
- $('#container').bind('cocoon:before-insert', function(e, insertedItem) {
347
+ $('#container').on('cocoon:before-insert', function(e, insertedItem) {
327
348
  // ... do something
328
349
  });
329
350
  ```
@@ -348,32 +369,32 @@ The callbacks can be added as follows:
348
369
  ```javascript
349
370
  $(document).ready(function() {
350
371
  $('#owner')
351
- .bind('cocoon:before-insert', function() {
372
+ .on('cocoon:before-insert', function() {
352
373
  $("#owner_from_list").hide();
353
374
  $("#owner a.add_fields").hide();
354
375
  })
355
- .bind('cocoon:after-insert', function() {
376
+ .on('cocoon:after-insert', function() {
356
377
  /* ... do something ... */
357
378
  })
358
- .bind("cocoon:before-remove", function() {
379
+ .on("cocoon:before-remove", function() {
359
380
  $("#owner_from_list").show();
360
381
  $("#owner a.add_fields").show();
361
382
  })
362
- .bind("cocoon:after-remove", function() {
383
+ .on("cocoon:after-remove", function() {
363
384
  /* e.g. recalculate order of child items */
364
385
  });
365
386
 
366
387
  // example showing manipulating the inserted/removed item
367
388
 
368
389
  $('#tasks')
369
- .bind('cocoon:before-insert', function(e,task_to_be_added) {
390
+ .on('cocoon:before-insert', function(e,task_to_be_added) {
370
391
  task_to_be_added.fadeIn('slow');
371
392
  })
372
- .bind('cocoon:after-insert', function(e, added_task) {
393
+ .on('cocoon:after-insert', function(e, added_task) {
373
394
  // e.g. set the background of inserted task
374
395
  added_task.css("background","red");
375
396
  })
376
- .bind('cocoon:before-remove', function(e, task) {
397
+ .on('cocoon:before-remove', function(e, task) {
377
398
  // allow some time for the animation to complete
378
399
  $(this).data('remove-timeout', 1000);
379
400
  task.fadeOut('slow');
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.2.1
@@ -7,14 +7,13 @@
7
7
  content.replace(reg_exp, with_str);
8
8
  }
9
9
 
10
-
11
10
  $(document).on('click', '.add_fields', function(e) {
12
11
  e.preventDefault();
13
12
  var $this = $(this),
14
13
  assoc = $this.data('association'),
15
14
  assocs = $this.data('associations'),
16
15
  content = $this.data('association-insertion-template'),
17
- insertionMethod = $this.data('association-insertion-method') || $this.data('association-insertion-position') || 'before';
16
+ insertionMethod = $this.data('association-insertion-method') || $this.data('association-insertion-position') || 'before',
18
17
  insertionNode = $this.data('association-insertion-node'),
19
18
  insertionTraversal = $this.data('association-insertion-traversal'),
20
19
  regexp_braced = new RegExp('\\[new_' + assoc + '\\](.*?\\s)', 'g'),
@@ -34,7 +33,7 @@
34
33
 
35
34
  if (insertionNode){
36
35
  if (insertionTraversal){
37
- insertionNode = $this[insertionTraversal](insertionNode)
36
+ insertionNode = $this[insertionTraversal](insertionNode);
38
37
  } else {
39
38
  insertionNode = insertionNode == "this" ? $this : $(insertionNode);
40
39
  }
@@ -54,7 +53,6 @@
54
53
  insertionNode.trigger('cocoon:after-insert', [contentNode]);
55
54
  });
56
55
 
57
-
58
56
  $(document).on('click', '.remove_fields.dynamic, .remove_fields.existing', function(e) {
59
57
  var $this = $(this);
60
58
  var node_to_delete = $this.closest(".nested-fields");
@@ -64,7 +62,6 @@
64
62
 
65
63
  trigger_node.trigger('cocoon:before-remove', [node_to_delete]);
66
64
 
67
-
68
65
  var timeout = trigger_node.data('remove-timeout') || 0;
69
66
 
70
67
  setTimeout(function() {
@@ -78,4 +75,6 @@
78
75
  }, timeout);
79
76
  });
80
77
 
78
+ $('.remove_fields.existing.destroyed').closest('.nested-fields').hide();
79
+
81
80
  })(jQuery);
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "cocoon"
8
- s.version = "1.2.0"
8
+ s.version = "1.2.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Nathan Van der Auwera"]
12
- s.date = "2013-07-10"
12
+ s.date = "2013-09-25"
13
13
  s.description = "Unobtrusive nested forms handling, using jQuery. Use this and discover cocoon-heaven."
14
14
  s.email = "nathan@dixis.com"
15
15
  s.extra_rdoc_files = [
@@ -25,18 +25,23 @@ module Cocoon
25
25
  html_options = args[2] || {}
26
26
 
27
27
  is_dynamic = f.object.new_record?
28
- html_options[:class] = [html_options[:class], "remove_fields #{is_dynamic ? 'dynamic' : 'existing'}"].compact.join(' ')
29
- hidden_field_tag("#{f.object_name}[_destroy]") + link_to(name, '#', html_options)
28
+
29
+ classes = []
30
+ classes << "remove_fields"
31
+ classes << (is_dynamic ? 'dynamic' : 'existing')
32
+ classes << 'destroyed' if f.object.marked_for_destruction?
33
+ html_options[:class] = [html_options[:class], classes.join(' ')].compact.join(' ')
34
+ hidden_field_tag("#{f.object_name}[_destroy]", f.object._destroy) + link_to(name, '#', html_options)
30
35
  end
31
36
  end
32
37
 
33
38
  # :nodoc:
34
- def render_association(association, f, new_object, render_options={}, custom_partial=nil)
39
+ def render_association(association, f, new_object, form_name, render_options={}, custom_partial=nil)
35
40
  partial = get_partial_path(custom_partial, association)
36
41
  locals = render_options.delete(:locals) || {}
37
42
  method_name = f.respond_to?(:semantic_fields_for) ? :semantic_fields_for : (f.respond_to?(:simple_fields_for) ? :simple_fields_for : :fields_for)
38
43
  f.send(method_name, association, new_object, {:child_index => "new_#{association}"}.merge(render_options)) do |builder|
39
- partial_options = {:f => builder, :dynamic => true}.merge(locals)
44
+ partial_options = {form_name.to_sym => builder, :dynamic => true}.merge(locals)
40
45
  render(partial, partial_options)
41
46
  end
42
47
  end
@@ -50,8 +55,9 @@ module Cocoon
50
55
  # - *:render_options* : options passed to `simple_fields_for, semantic_fields_for or fields_for`
51
56
  # - *:locals* : the locals hash in the :render_options is handed to the partial
52
57
  # - *:partial* : explicitly override the default partial name
53
- # - *:wrap_object : !!! document more here !!!
54
- # - *!!!add some option to build in collection or not!!!*
58
+ # - *:wrap_object* : a proc that will allow to wrap your object, especially suited when using
59
+ # decorators, or if you want special initialisation
60
+ # - *:form_name* : the parameter for the form in the nested form partial. Default `f`.
55
61
  # - *&block*: see <tt>link_to</tt>
56
62
 
57
63
  def link_to_add_association(*args, &block)
@@ -71,6 +77,7 @@ module Cocoon
71
77
  override_partial = html_options.delete(:partial)
72
78
  wrap_object = html_options.delete(:wrap_object)
73
79
  force_non_association_create = html_options.delete(:force_non_association_create) || false
80
+ form_parameter_name = html_options.delete(:form_name) || 'f'
74
81
 
75
82
  html_options[:class] = [html_options[:class], "add_fields"].compact.join(' ')
76
83
  html_options[:'data-association'] = association.to_s.singularize
@@ -79,7 +86,7 @@ module Cocoon
79
86
  new_object = create_object(f, association, force_non_association_create)
80
87
  new_object = wrap_object.call(new_object) if wrap_object.respond_to?(:call)
81
88
 
82
- html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, render_options, override_partial)).html_safe
89
+ html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, form_parameter_name, render_options, override_partial)).html_safe
83
90
 
84
91
  link_to(name, '#', html_options )
85
92
  end
@@ -111,12 +118,18 @@ module Cocoon
111
118
  if instance.class.name == "Mongoid::Relations::Metadata" || force_non_association_create
112
119
  create_object_with_conditions(instance)
113
120
  else
121
+ assoc_obj = nil
122
+
114
123
  # assume ActiveRecord or compatible
115
124
  if instance.collection?
116
- f.object.send(association).build
125
+ assoc_obj = f.object.send(association).build
126
+ f.object.send(association).delete(assoc_obj)
117
127
  else
118
- f.object.send("build_#{association}")
128
+ assoc_obj = f.object.send("build_#{association}")
129
+ f.object.send(association).delete
119
130
  end
131
+
132
+ assoc_obj
120
133
  end
121
134
  end
122
135
 
@@ -44,7 +44,7 @@ describe Cocoon do
44
44
  context "and explicitly specifying the wanted partial" do
45
45
  before do
46
46
  @tester.unstub(:render_association)
47
- @tester.should_receive(:render_association).with(anything(), anything(), anything(), anything(), "shared/partial").and_return('partiallll')
47
+ @tester.should_receive(:render_association).with(anything(), anything(), anything(), "f", anything(), "shared/partial").and_return('partiallll')
48
48
  @html = @tester.link_to_add_association('add something', @form_obj, :comments, :partial => "shared/partial")
49
49
  end
50
50
 
@@ -53,7 +53,7 @@ describe Cocoon do
53
53
 
54
54
  it "gives an opportunity to wrap/decorate created objects" do
55
55
  @tester.unstub(:render_association)
56
- @tester.should_receive(:render_association).with(anything(), anything(), kind_of(CommentDecorator), anything(), anything()).and_return('partiallll')
56
+ @tester.should_receive(:render_association).with(anything(), anything(), kind_of(CommentDecorator), "f", anything(), anything()).and_return('partiallll')
57
57
  @tester.link_to_add_association('add something', @form_obj, :comments, :wrap_object => Proc.new {|comment| CommentDecorator.new(comment) })
58
58
  end
59
59
 
@@ -128,7 +128,7 @@ describe Cocoon do
128
128
  context "and explicitly specifying the wanted partial" do
129
129
  before do
130
130
  @tester.unstub(:render_association)
131
- @tester.should_receive(:render_association).with(anything(), anything(), anything(), anything(), "shared/partial").and_return('partiallll')
131
+ @tester.should_receive(:render_association).with(anything(), anything(), anything(), "f", anything(), "shared/partial").and_return('partiallll')
132
132
  @html = @tester.link_to_add_association( @form_obj, :comments, :class => 'floppy disk', :partial => "shared/partial") do
133
133
  "some long name"
134
134
  end
@@ -163,7 +163,7 @@ describe Cocoon do
163
163
  context "with extra render-options for rendering the child relation" do
164
164
  context "uses the correct plural" do
165
165
  before do
166
- @tester.should_receive(:render_association).with(:people, @form_obj, anything, {:wrapper => 'inline'}, nil)
166
+ @tester.should_receive(:render_association).with(:people, @form_obj, anything, "f", {:wrapper => 'inline'}, nil)
167
167
  @html = @tester.link_to_add_association('add something', @form_obj, :people, :render_options => {:wrapper => 'inline'})
168
168
  end
169
169
  it_behaves_like "a correctly rendered add link", {association: 'person', associations: 'people' }
@@ -193,6 +193,17 @@ describe Cocoon do
193
193
  end
194
194
  end
195
195
 
196
+ context "overruling the form parameter name" do
197
+ context "when given a form_name it passes it correctly to the partials" do
198
+ before do
199
+ @tester.unstub(:render_association)
200
+ @form_obj.should_receive(:fields_for) { | association, new_object, options_hash, &block| block.call }
201
+ @tester.should_receive(:render).with("person_fields", {:people_form => nil, :dynamic=>true}).and_return ("partiallll")
202
+ @html = @tester.link_to_add_association('add something', @form_obj, :people, :form_name => 'people_form')
203
+ end
204
+ it_behaves_like "a correctly rendered add link", {template: 'partiallll', association: 'person', associations: 'people' }
205
+ end
206
+ end
196
207
 
197
208
 
198
209
  context "when using formtastic" do
@@ -241,6 +252,7 @@ describe Cocoon do
241
252
  removed = doc.at('input')
242
253
  removed.attribute('id').value.should == "Post__destroy"
243
254
  removed.attribute('name').value.should == "Post[_destroy]"
255
+ removed.attribute('value').value.should == "false"
244
256
  end
245
257
 
246
258
  it_behaves_like "a correctly rendered remove link", {}
@@ -255,6 +267,27 @@ describe Cocoon do
255
267
 
256
268
  end
257
269
 
270
+ # this is needed when due to some validation error, objects that
271
+ # were already marked for destruction need to remain hidden
272
+ context "for a object marked for destruction" do
273
+ before do
274
+ @post_marked_for_destruction = Post.new
275
+ @post_marked_for_destruction.mark_for_destruction
276
+ @form_obj_destroyed = double(:object => @post_marked_for_destruction, :object_name => @post_marked_for_destruction.class.name)
277
+ @html = @tester.link_to_remove_association('remove something', @form_obj_destroyed)
278
+ end
279
+
280
+ it "is rendered inside a input element" do
281
+ doc = Nokogiri::HTML(@html)
282
+ removed = doc.at('input')
283
+ removed.attribute('id').value.should == "Post__destroy"
284
+ removed.attribute('name').value.should == "Post[_destroy]"
285
+ removed.attribute('value').value.should == "true"
286
+ end
287
+
288
+ it_behaves_like "a correctly rendered remove link", {class: 'remove_fields dynamic destroyed'}
289
+ end
290
+
258
291
  context "with a block" do
259
292
  context "the block gives the name" do
260
293
  before do
@@ -287,11 +320,15 @@ describe Cocoon do
287
320
  end
288
321
  result = @tester.create_object(@form_obj, :admin_comments)
289
322
  result.author.should == "Admin"
323
+ @form_obj.object.admin_comments.should be_empty
290
324
  end
291
325
 
292
326
  it "creates correct association for belongs_to associations" do
293
- result = @tester.create_object(double(:object => Comment.new), :post)
327
+ comment = Comment.new
328
+ form_obj = double(:object => Comment.new)
329
+ result = @tester.create_object(form_obj, :post)
294
330
  result.should be_a Post
331
+ comment.post.should be_nil
295
332
  end
296
333
 
297
334
  it "raises an error if cannot reflect on association" do
@@ -6,8 +6,7 @@ Dummy::Application.configure do
6
6
  # since you don't have to restart the webserver when you make code changes.
7
7
  config.cache_classes = false
8
8
 
9
- # Log error messages when you accidentally call methods on nil.
10
- config.whiny_nils = true
9
+ config.eager_load = false
11
10
 
12
11
  # Show full error reports and disable caching
13
12
  config.consider_all_requests_local = true
@@ -4,6 +4,7 @@ Dummy::Application.configure do
4
4
  # The production environment is meant for finished, "live" apps.
5
5
  # Code is not reloaded between requests
6
6
  config.cache_classes = true
7
+ config.eager_load = true
7
8
 
8
9
  # Full error reports are disabled and caching is turned on
9
10
  config.consider_all_requests_local = false
@@ -6,9 +6,7 @@ Dummy::Application.configure do
6
6
  # your test database is "scratch space" for the test suite and is wiped
7
7
  # and recreated between test runs. Don't rely on the data there!
8
8
  config.cache_classes = true
9
-
10
- # Log error messages when you accidentally call methods on nil.
11
- config.whiny_nils = true
9
+ config.eager_load = false
12
10
 
13
11
  # Show full error reports and disable caching
14
12
  config.consider_all_requests_local = true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocoon
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-10 00:00:00.000000000 Z
12
+ date: 2013-09-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -270,7 +270,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
270
270
  version: '0'
271
271
  segments:
272
272
  - 0
273
- hash: 171754953178404834
273
+ hash: -3336163965730752832
274
274
  required_rubygems_version: !ruby/object:Gem::Requirement
275
275
  none: false
276
276
  requirements: