cocoon 1.1.0 → 1.1.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,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: