cocoon 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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: