cocoon 1.1.0 → 1.1.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,10 @@
1
1
  # Change History / Release Notes
2
2
 
3
+ ## Version 1.1.1
4
+
5
+ * added the to be added/deleted element to the event, this allows to add animations/actions onto them
6
+ * added extra option :wrap_object, allowing to use Decorators instead of the association object
7
+ * added an option :force_non_association_create, that will allow to use `link_to_add_association` inside the fields-partial
3
8
 
4
9
  ## Version 1.1.0
5
10
 
data/README.markdown CHANGED
@@ -216,6 +216,8 @@ It takes four parameters:
216
216
  - `partial`: explicitly declare the name of the partial that will be used
217
217
  - `render_options` : options passed through to the form-builder function (e.g. `simple_fields_for`, `semantic_fields_for` or `fields_for`).
218
218
  If it contains a `:locals` option containing a hash, that is handed to the partial.
219
+ - `wrap_object` : a proc that will allow to wrap your object, especially useful if you are using decorators (e.g. draper). See example lower.
220
+ - `force_non_association_create`: if true, it will _not_ create the new object using the association (see lower)
219
221
 
220
222
  Optionally you could also leave out the name and supply a block that is captured to give the name (if you want to do something more complicated).
221
223
 
@@ -245,6 +247,65 @@ To overrule the default partial name, e.g. because it shared between multiple vi
245
247
  = link_to_add_association 'add something', f, :something, :partial => 'shared/something_fields'
246
248
  ````
247
249
 
250
+ #### :wrap_object
251
+
252
+ If you are using decorators, the normal instantiation of the associated will not be enough, actually you want to generate the decorated object.
253
+
254
+ A simple decorator would look like:
255
+
256
+ ```
257
+ class CommentDecorator
258
+ def initialize(comment)
259
+ @comment = comment
260
+ end
261
+
262
+ def formatted_created_at
263
+ @comment.created_at.to_formatted_s(:short)
264
+ end
265
+
266
+ def method_missing(method_sym, *args)
267
+ if @comment.respond_to?(method_sym)
268
+ @comment.send(method_sym, *args)
269
+ else
270
+ super
271
+ end
272
+ end
273
+ end
274
+ ```
275
+
276
+ To use this, write
277
+
278
+ ```
279
+ link_to_add_association('add something', @form_obj, :comments, :wrap_object => Proc.new {|comment| CommentDecorator.new(comment) })
280
+ ```
281
+
282
+ Note that the `:wrap_object` expects an object that is _callable_, so any `Proc` will do. So you could as well use it to do some fancy extra initialisation (if needed).
283
+ But note you will have to return the (nested) object you want used.
284
+ E.g.
285
+
286
+
287
+ ```
288
+ link_to_add_association('add something', @form_obj, :comments,
289
+ :wrap_object => Proc.new { |comment| comment.name = current_user.name; comment })
290
+ ```
291
+
292
+ #### :force_non_association_create
293
+
294
+ In normal cases we create a new nested object using the association relation itself. This is the cleanest way to create
295
+ 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.
296
+
297
+ 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
298
+ in an infinite loop.
299
+
300
+ To resolve this, specify that `:force_non_association_create` should be `true`, as follows:
301
+
302
+ ```
303
+ link_to_add_association('add something', @form_obj, :comments, :force_non_association_create => true)
304
+ ```
305
+
306
+ By default `:force_non_association_create` is `false`.
307
+
308
+ > A cleaner option would be to call a function that performs this initialisation and returns `self` at the end.
248
309
 
249
310
  ### link_to_remove_association
250
311
 
@@ -269,6 +330,16 @@ On insertion or removal the following events are triggered:
269
330
  * `cocoon:before-remove`: called before removing the nested child
270
331
  * `cocoon:after-remove`: called after removal
271
332
 
333
+ To listen to the events, you to have the following code in your javascript:
334
+
335
+ $('#container').bind('cocoon:before-insert', function(e, inserted_item) {
336
+ // ... do something
337
+ });
338
+
339
+ where `e` is the event and the second parameter is the inserted or removed item. This allows you to change markup, or
340
+ add effects/animations (see example below).
341
+
342
+
272
343
  If in your view you have the following snippet to select an `owner`
273
344
  (we use slim for demonstration purposes)
274
345
 
@@ -304,12 +375,38 @@ $(document).ready(function() {
304
375
  function() {
305
376
  /* e.g. recalculate order of child items */
306
377
  });
378
+
379
+ // example showing manipulating the inserted/removed item
380
+
381
+ $('#tasks').bind('cocoon:before-insert', function(e,task_to_be_added) {
382
+ task_to_be_added.fadeIn('slow');
383
+ });
384
+
385
+ $('#tasks').bind('cocoon:after-insert', function(e, added_task) {
386
+ // e.g. set the background of inserted task
387
+ added_task.css("background","red");
388
+ });
389
+
390
+ $('#tasks').bind('cocoon:before-remove', function(e, task) {
391
+ // allow some time for the animation to complete
392
+ $(this).data('remove-timeout', 1000);
393
+ task.fadeOut('slow');
394
+ })
395
+
396
+
307
397
  });
308
398
  ````
309
399
 
310
400
  Do note that for the callbacks to work there has to be a surrounding container (div), where you can bind the callbacks to.
311
401
 
312
402
 
403
+ When adding animations and effects to make the removal of items more interesting, you will also have to provide a timeout.
404
+ This is accomplished by the following line:
405
+
406
+ $(this).data('remove-timeout', 1000);
407
+
408
+ Note that you could also immediately add this to your view (on the `.nested-fields` container).
409
+
313
410
  ### Control the Insertion behaviour
314
411
 
315
412
  The default insertion location is at the back of the current container. But we have added two `data`-attributes that are read to determine the insertion-node and -method.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.1.1
@@ -1,17 +1,12 @@
1
1
  (function($) {
2
2
 
3
+ var cocoon_element_counter = 0;
4
+
3
5
  function replace_in_content(content, regexp_str, with_str) {
4
6
  reg_exp = new RegExp(regexp_str);
5
7
  content.replace(reg_exp, with_str);
6
8
  }
7
9
 
8
- function trigger_before_removal_callback(node) {
9
- node.trigger('cocoon:before-remove');
10
- }
11
-
12
- function trigger_after_removal_callback(node) {
13
- node.trigger('cocoon:after-remove');
14
- }
15
10
 
16
11
  $('.add_fields').live('click', function(e) {
17
12
  e.preventDefault();
@@ -22,20 +17,20 @@
22
17
  insertionMethod = $this.data('association-insertion-method') || $this.data('association-insertion-position') || 'before';
23
18
  insertionNode = $this.data('association-insertion-node'),
24
19
  insertionTraversal = $this.data('association-insertion-traversal'),
25
- regexp_braced = new RegExp('\\[new_' + assoc + '\\]', 'g'),
26
- regexp_underscord = new RegExp('_new_' + assoc + '_', 'g'),
27
- new_id = new Date().getTime(),
20
+ regexp_braced = new RegExp('\\[new_' + assoc + '\\](.*?\\s)', 'g'),
21
+ regexp_underscord = new RegExp('_new_' + assoc + '_(\\w*)', 'g'),
22
+ new_id = new Date().getTime() + cocoon_element_counter++,
28
23
  newcontent_braced = '[' + new_id + ']',
29
24
  newcontent_underscord = '_' + new_id + '_',
30
- new_content = content.replace(regexp_braced, '[' + new_id + ']');
25
+ new_content = content.replace(regexp_braced, '[' + new_id + ']$1');
31
26
 
32
27
  if (new_content == content) {
33
- regexp_braced = new RegExp('\\[new_' + assocs + '\\]', 'g');
34
- regexp_underscord = new RegExp('_new_' + assocs + '_', 'g');
35
- new_content = content.replace(regexp_braced, '[' + new_id + ']');
28
+ regexp_braced = new RegExp('\\[new_' + assocs + '\\](.*?\\s)', 'g');
29
+ regexp_underscord = new RegExp('_new_' + assocs + '_(\\w*)', 'g');
30
+ new_content = content.replace(regexp_braced, '[' + new_id + ']$1');
36
31
  }
37
32
 
38
- new_content = new_content.replace(regexp_underscord, newcontent_underscord);
33
+ new_content = new_content.replace(regexp_underscord, newcontent_underscord + "$1");
39
34
 
40
35
  if (insertionNode){
41
36
  if (insertionTraversal){
@@ -49,33 +44,38 @@
49
44
 
50
45
  var contentNode = $(new_content);
51
46
 
52
- insertionNode.trigger('cocoon:before-insert');
47
+ insertionNode.trigger('cocoon:before-insert', [contentNode]);
53
48
 
54
49
  // allow any of the jquery dom manipulation methods (after, before, append, prepend, etc)
55
50
  // to be called on the node. allows the insertion node to be the parent of the inserted
56
51
  // code and doesn't force it to be a sibling like after/before does. default: 'before'
57
- insertionNode[insertionMethod](contentNode);
52
+ var addedContent = insertionNode[insertionMethod](contentNode);
58
53
 
59
- insertionNode.trigger('cocoon:after-insert');
54
+ insertionNode.trigger('cocoon:after-insert', [contentNode]);
60
55
  });
61
56
 
62
- $('.remove_fields.dynamic').live('click', function(e) {
63
- var $this = $(this);
64
- var trigger_node = $this.closest(".nested-fields").parent();
65
- trigger_before_removal_callback(trigger_node);
66
- e.preventDefault();
67
- $this.closest(".nested-fields").remove();
68
- trigger_after_removal_callback(trigger_node);
69
- });
70
57
 
71
- $('.remove_fields.existing').live('click', function(e) {
58
+ $('.remove_fields.dynamic, .remove_fields.existing').live('click', function(e) {
72
59
  var $this = $(this);
73
- var trigger_node = $this.closest(".nested-fields").parent().parent();
74
- trigger_before_removal_callback(trigger_node);
60
+ var node_to_delete = $this.closest(".nested-fields");
61
+ var trigger_node = node_to_delete.parent();
62
+
75
63
  e.preventDefault();
76
- $this.prev("input[type=hidden]").val("1");
77
- $this.closest(".nested-fields").hide();
78
- trigger_after_removal_callback(trigger_node);
64
+
65
+ trigger_node.trigger('cocoon:before-remove', [node_to_delete]);
66
+
67
+
68
+ var timeout = trigger_node.data('remove-timeout') || 0;
69
+
70
+ setTimeout(function() {
71
+ if ($this.hasClass('dynamic')) {
72
+ $this.closest(".nested-fields").remove();
73
+ } else {
74
+ $this.prev("input[type=hidden]").val("1");
75
+ $this.closest(".nested-fields").hide();
76
+ }
77
+ trigger_node.trigger('cocoon:after-remove', [node_to_delete]);
78
+ }, timeout);
79
79
  });
80
80
 
81
81
  })(jQuery);
data/cocoon.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "cocoon"
8
- s.version = "1.1.0"
8
+ s.version = "1.1.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 = "2012-10-08"
12
+ s.date = "2012-11-22"
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 = [
@@ -50,6 +50,8 @@ module Cocoon
50
50
  # - *:render_options* : options passed to `simple_fields_for, semantic_fields_for or fields_for`
51
51
  # - *:locals* : the locals hash in the :render_options is handed to the partial
52
52
  # - *:partial* : explicitly override the default partial name
53
+ # - *:wrap_object : !!! document more here !!!
54
+ # - *!!!add some option to build in collection or not!!!*
53
55
  # - *&block*: see <tt>link_to</tt>
54
56
 
55
57
  def link_to_add_association(*args, &block)
@@ -68,16 +70,14 @@ module Cocoon
68
70
  render_options ||= {}
69
71
  override_partial = html_options.delete(:partial)
70
72
  wrap_object = html_options.delete(:wrap_object)
73
+ force_non_association_create = html_options.delete(:force_non_association_create) || false
71
74
 
72
75
  html_options[:class] = [html_options[:class], "add_fields"].compact.join(' ')
73
76
  html_options[:'data-association'] = association.to_s.singularize
74
77
  html_options[:'data-associations'] = association.to_s.pluralize
75
78
 
76
- if wrap_object.respond_to?(:call)
77
- new_object = wrap_object.call(create_object(f, association))
78
- else
79
- new_object = create_object(f, association)
80
- end
79
+ new_object = create_object(f, association, force_non_association_create)
80
+ new_object = wrap_object.call(new_object) if wrap_object.respond_to?(:call)
81
81
 
82
82
  html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, render_options, override_partial)).html_safe
83
83
 
@@ -89,10 +89,10 @@ module Cocoon
89
89
  # `` has_many :admin_comments, class_name: "Comment", conditions: { author: "Admin" }
90
90
  # will create new Comment with author "Admin"
91
91
 
92
- def create_object(f, association)
92
+ def create_object(f, association, force_non_association_create=false)
93
93
  assoc = f.object.class.reflect_on_association(association)
94
94
 
95
- assoc ? create_object_on_association(f, association, assoc) : create_object_on_non_association(f, association)
95
+ assoc ? create_object_on_association(f, association, assoc, force_non_association_create) : create_object_on_non_association(f, association)
96
96
  end
97
97
 
98
98
  def get_partial_path(partial, association)
@@ -107,13 +107,12 @@ module Cocoon
107
107
  raise "Association #{association} doesn't exist on #{f.object.class}"
108
108
  end
109
109
 
110
- def create_object_on_association(f, association, instance)
111
- if instance.class.name == "Mongoid::Relations::Metadata"
112
- conditions = instance.respond_to?(:conditions) ? instance.conditions.flatten : []
113
- instance.klass.new(*conditions)
110
+ def create_object_on_association(f, association, instance, force_non_association_create)
111
+ if instance.class.name == "Mongoid::Relations::Metadata" || force_non_association_create
112
+ create_object_with_conditions(instance)
114
113
  else
115
114
  # assume ActiveRecord or compatible
116
- if instance.collection?
115
+ if instance.collection?
117
116
  f.object.send(association).build
118
117
  else
119
118
  f.object.send("build_#{association}")
@@ -121,5 +120,10 @@ module Cocoon
121
120
  end
122
121
  end
123
122
 
123
+ def create_object_with_conditions(instance)
124
+ conditions = instance.respond_to?(:conditions) ? instance.conditions.flatten : []
125
+ instance.klass.new(*conditions)
126
+ end
127
+
124
128
  end
125
129
  end
data/spec/cocoon_spec.rb CHANGED
@@ -10,11 +10,15 @@ describe Cocoon do
10
10
  it { should respond_to(:link_to_add_association) }
11
11
  it { should respond_to(:link_to_remove_association) }
12
12
 
13
+ before(:each) do
14
+ @tester = TestClass.new
15
+ @post = Post.new
16
+ @form_obj = stub(:object => @post, :object_name => @post.class.name)
17
+ end
18
+
19
+
13
20
  context "link_to_add_association" do
14
21
  before(:each) do
15
- @tester = TestClass.new
16
- @post = Post.new
17
- @form_obj = stub(:object => @post)
18
22
  @tester.stub(:render_association).and_return('form<tag>')
19
23
  end
20
24
 
@@ -41,6 +45,25 @@ describe Cocoon do
41
45
  @tester.should_receive(:render_association).with(anything(), anything(), kind_of(CommentDecorator), anything(), anything()).and_return('partiallll')
42
46
  @tester.link_to_add_association('add something', @form_obj, :comments, :wrap_object => Proc.new {|comment| CommentDecorator.new(comment) })
43
47
  end
48
+
49
+ context "force non association create" do
50
+ it "default it uses the association" do
51
+ @tester.should_receive(:create_object).with(anything, :comments , false)
52
+ result = @tester.link_to_add_association('add something', @form_obj, :comments)
53
+ result.to_s.should == '<a href="#" class="add_fields" data-association-insertion-template="form&lt;tag&gt;" data-association="comment" data-associations="comments">add something</a>'
54
+ end
55
+ it "specifying false is the same as default: create object on association" do
56
+ @tester.should_receive(:create_object).with(anything, :comments , false)
57
+ result = @tester.link_to_add_association('add something', @form_obj, :comments, :force_non_association_create => false)
58
+ result.to_s.should == '<a href="#" class="add_fields" data-association-insertion-template="form&lt;tag&gt;" data-association="comment" data-associations="comments">add something</a>'
59
+ end
60
+ it "specifying true will not create objects on association but using the conditions" do
61
+ @tester.should_receive(:create_object).with(anything, :comments , true)
62
+ result = @tester.link_to_add_association('add something', @form_obj, :comments, :force_non_association_create => true)
63
+ result.to_s.should == '<a href="#" class="add_fields" data-association-insertion-template="form&lt;tag&gt;" data-association="comment" data-associations="comments">add something</a>'
64
+ end
65
+
66
+ end
44
67
  end
45
68
 
46
69
  context "with a block" do
@@ -146,12 +169,6 @@ describe Cocoon do
146
169
  end
147
170
 
148
171
  context "link_to_remove_association" do
149
- before(:each) do
150
- @tester = TestClass.new
151
- @post = Post.new
152
- @form_obj = stub(:object => @post, :object_name => @post.class.name)
153
- end
154
-
155
172
  context "without a block" do
156
173
  it "accepts a name" do
157
174
  result = @tester.link_to_remove_association('remove something', @form_obj)
@@ -180,44 +197,50 @@ describe Cocoon do
180
197
  result.to_s.should == "<input id=\"Post__destroy\" name=\"Post[_destroy]\" type=\"hidden\" /><a href=\"#\" class=\"add_some_class remove_fields dynamic\" data-something=\"bla\">remove some long name</a>"
181
198
  end
182
199
  end
200
+ end
183
201
 
184
- context "create_object" do
185
- it "should create correct association with conditions" do
186
- result = @tester.create_object(@form_obj, :admin_comments)
187
- result.author.should == "Admin"
188
- end
202
+ context "create_object" do
203
+ it "creates correct association with conditions" do
204
+ @tester.should_not_receive(:create_object_with_conditions)
205
+ result = @tester.create_object(@form_obj, :admin_comments)
206
+ result.author.should == "Admin"
207
+ end
189
208
 
190
- it "should create correct association for belongs_to associations" do
191
- result = @tester.create_object(stub(:object => Comment.new), :post)
192
- result.should be_a Post
193
- end
209
+ it "creates correct association for belongs_to associations" do
210
+ result = @tester.create_object(stub(:object => Comment.new), :post)
211
+ result.should be_a Post
212
+ end
194
213
 
195
- it "should raise error if cannot reflect on association" do
196
- expect { @tester.create_object(stub(:object => Comment.new), :not_existing) }.to raise_error /association/i
197
- end
214
+ it "raises an error if cannot reflect on association" do
215
+ expect { @tester.create_object(stub(:object => Comment.new), :not_existing) }.to raise_error /association/i
216
+ end
198
217
 
199
- it "should create an association if object responds to 'build_association' as singular" do
200
- object = Comment.new
201
- object.should_receive(:build_custom_item).and_return 'custom'
202
- @tester.create_object(stub(:object => object), :custom_item).should == 'custom'
203
- end
218
+ it "creates an association if object responds to 'build_association' as singular" do
219
+ object = Comment.new
220
+ object.should_receive(:build_custom_item).and_return 'custom'
221
+ @tester.create_object(stub(:object => object), :custom_item).should == 'custom'
222
+ end
204
223
 
205
- it "should create an association if object responds to 'build_association' as plural" do
206
- object = Comment.new
207
- object.should_receive(:build_custom_item).and_return 'custom'
208
- @tester.create_object(stub(:object => object), :custom_items).should == 'custom'
209
- end
224
+ it "creates an association if object responds to 'build_association' as plural" do
225
+ object = Comment.new
226
+ object.should_receive(:build_custom_item).and_return 'custom'
227
+ @tester.create_object(stub(:object => object), :custom_items).should == 'custom'
210
228
  end
211
229
 
212
- context "get_partial_path" do
213
- it "generates the default partial name if no partial given" do
214
- result = @tester.get_partial_path(nil, :admin_comments)
215
- result.should == "admin_comment_fields"
216
- end
217
- it "uses the given partial name" do
218
- result = @tester.get_partial_path("comment_fields", :admin_comments)
219
- result.should == "comment_fields"
220
- end
230
+ it "can create using only conditions not the association" do
231
+ @tester.should_receive(:create_object_with_conditions).and_return('flappie')
232
+ @tester.create_object(@form_obj, :comments, true).should == 'flappie'
233
+ end
234
+ end
235
+
236
+ context "get_partial_path" do
237
+ it "generates the default partial name if no partial given" do
238
+ result = @tester.get_partial_path(nil, :admin_comments)
239
+ result.should == "admin_comment_fields"
240
+ end
241
+ it "uses the given partial name" do
242
+ result = @tester.get_partial_path("comment_fields", :admin_comments)
243
+ result.should == "comment_fields"
221
244
  end
222
245
  end
223
246
 
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.1.0
4
+ version: 1.1.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: 2012-10-08 00:00:00.000000000 Z
12
+ date: 2012-11-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -248,7 +248,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
248
248
  version: '0'
249
249
  segments:
250
250
  - 0
251
- hash: -4012454906623253900
251
+ hash: -2830798901960136537
252
252
  required_rubygems_version: !ruby/object:Gem::Requirement
253
253
  none: false
254
254
  requirements: