awesome_nested_fields 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -26,7 +26,7 @@ Installation
26
26
 
27
27
  gem 'awesome_nested_fields'
28
28
 
29
- 2. Copy the javascript dependency to `public\javascripts` by using the generator.
29
+ 2. Copy the javascript dependency to `public/javascripts` by using the generator.
30
30
 
31
31
  rails generate awesome_nested_fields:install
32
32
 
@@ -44,62 +44,144 @@ Basic Usage
44
44
 
45
45
  First, make sure the object that has the `has_many` or `has_and_belongs_to_many` relation accepts nested attributes for the collection you want. For example, if a person _has_many_ phones, we'll have a model like this:
46
46
 
47
- class Person < ActiveRecord::Base
48
- has_many :phones
49
- accepts_nested_attributes_for :phones, allow_destroy: true
50
- end
47
+ class Person < ActiveRecord::Base
48
+ has_many :phones
49
+ accepts_nested_attributes_for :phones, allow_destroy: true
50
+ end
51
51
 
52
52
  The `accepts_nested_attributes_for` is a method from Active Record that allows you to pass attributes of nested models directly to its parent, instead of instantiate each child object separately. In this case, `Person` gains a method called `phones_attributes=`, that accepts data for new and existing phones of a given person. The `allow_destroy` option enables us to also delete child objects. To know more about nested attributes, check out the [ActiveRecord::NestedAttribute](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L1) class.
53
53
 
54
54
  ### View
55
55
 
56
- The next step is set up the form view using the `nested_fields` helper method. It receives three parameters: the parent form builder, the association name and an optional hash of options (humm, a pun).
57
- Proceeding with the person/phones example, we can have a form like this:
56
+ The next step is set up the form view with the `nested_fields_for` method. It receives the association/collection name, an optional hash of options (humm, a pun) and a block with the nested fields. Proceeding with the person/phones example, we can have a form like this:
57
+
58
+ <%= form_for(@person) do |f| %>
59
+ <% # person fields... %>
60
+
61
+ <h2>Phones</h2>
62
+ <div class="container">
63
+ <%= f.nested_fields_for :phones do |f| %>
64
+ <fieldset class="item">
65
+ <%= f.label :number %>
66
+ <%= f.text_field :number %>
67
+
68
+ <a href="#" class="remove">remove</a>
69
+
70
+ <%= f.hidden_field :id %>
71
+ <%= f.hidden_field :_destroy %>
72
+ </fieldset>
73
+ <% end %>
74
+ </div>
75
+ <a href="#" class="add">add phone</a>
76
+
77
+ <% # more person fields... %>
78
+ <% end %>
58
79
 
59
- <%= form_for(@person) do |f| %>
60
- <% # person fields... %>
80
+ The `nested_fields_for` method lists the phones this person has and also adds an empty template to the page for creating new phones. (Actually, there is too much code inside the block. If you're not working with a simple example like this you better extract this code into a partial and call just `render :phones` inside the block. Good coding practices, you know.)
61
81
 
62
- <h2>Phones</h2>
63
- <div class="container">
64
- <%= nested_fields(f, :phones) %>
65
- </div>
66
- <a href="#" class="add">add phone</a>
82
+ If you're paying attention, you noticed the key elements are marked with special class names. We *need* this for the javascript code, so it knows what to do with each HTML element: the one that have the children must have the class `container`; each child must be marked with the class `item`; inside an item, the link for removal must have the class `remove`; and the link to add new items must have the class `add`. We can change the names later, but these are the default choices. Finally, don't forget to add the `id` field, as it is needed by AR to identify whether this is an existing or a new element, and the `_destroy` field to activate deletion when the user clicks on the remove link.
67
83
 
68
- <% # more person fields... %>
69
- <% end %>
84
+ ### Javascript
70
85
 
71
- The `nested_fields` helper lists the phones this person has and also adds an empty template to the page for creating new phones. But where is the phone form? Well, awesome_nested_fields expects a partial with the association name in the singular (after all, the partial represents a single child object). In this case, it looks for the partial `phone` (we can change this name later). So, in the file `_phone.html.erb`, we can have:
86
+ This is the easiest part: just activate the nested fields actions when the page loads. We can put this in the `application.js` file (or in any other place that gets executed in the page):
72
87
 
73
- <fieldset class="item">
74
- <%= f.label :where %>
75
- <%= f.text_field :where %><br/>
76
-
77
- <%= f.label :number %>
78
- <%= f.text_field :number %>
88
+ $(document).ready(function(e) {
89
+ $('FORM').nestedFields();
90
+ });
79
91
 
80
- <a href="#" class="remove">remove</a>
81
-
82
- <%= f.hidden_field :id %>
83
- <%= f.hidden_field :_destroy %>
84
- </fieldset>
92
+ Now enjoy your new nested model form!
85
93
 
86
- If you're paying attention, you noticed the key elements are marked with a special class name. We need this for the javascript code, so it knows what to do with each HTML element: the one that have the children must have the class `container`; each child must be marked with the class `item`; inside an item, the link for removal must have the class `remove`; and the link to add new items must have the class `add`. We can change the names later, but these are the default choices. Finally, don't forget to add the `id` field, as it is needed by AR to identify if this is an existing or a new element, and the `_destroy` field to activate deletion when the user clicks on the remove link.
87
94
 
88
- ### Javascript
95
+ Reference
96
+ ---------
89
97
 
90
- This is the easiest part: just activate the nested fields actions when the page loads. We can put this in the `application.js` file (or in any other place that gets executed in the page):
98
+ ### View Options
99
+
100
+ There are some view options, but most are internal. There is just one you really need to know about; for the others, go to the code.
101
+
102
+ #### show_empty
103
+
104
+ Sometimes you want to show something when the collection is empty. Just set `show_empty` to `true` and prepare the block to receive `nil` when the collection is empty. Awesome nested fields will take care to show the empty message when there are no elements and remove it when one is added.
105
+ To implement this on the basic example, do something like:
106
+
107
+ <%= f.nested_fields_for :phones, show_empty: true do |f| %>
108
+ <% if f %>
109
+ <% fields code... %>
110
+ <% else %>
111
+ <p class="empty">There are no phones.</p>
112
+ <% end %>
113
+ <% end %>
114
+
115
+ And yeah, you need to mark it with the class `empty` or any other selector configured via javascript.
91
116
 
92
- $(document).ready(function(e) {
93
- $('FORM').nestedFields();
117
+ ### Javascript Options
118
+
119
+ #### Selectors
120
+
121
+ To make nested fields work dynamically, the JS code needs to know what elements to use. By default, this is made by marking key elements with CSS classes, but you can use other selectors (any valid jQuery selector will do). The available options are shown below.
122
+
123
+ * `itemSelector` marks each item from the collection (`.item` by default)
124
+ * `containerSelector` marks the element that contains the items (`.container` by default)
125
+ * `addSelector` marks the element that will add a new item to the container when clicked (`.add` by default)
126
+ * `removeSelector` marks the element inside an item that will remove it when clicked (`.remove` by default)
127
+ * `emptySelector` marks the element that is shown when there are no items; used in conjunction with `show_empty` option (`.empty` by default)
128
+
129
+ For example, if you are using nested fields inside a table, you can do:
130
+
131
+ element.nestedFields({
132
+ containerSelector: 'tbody',
133
+ itemSelector: 'tr'
134
+ });
135
+
136
+
137
+ #### Callbacks
138
+
139
+ Actions can be executed before or after items get inserted or removed. There are four callbacks available: `beforeInsert`, `afterInsert`, `beforeRemove` and `afterRemove`. All of them receive the item as the first parameter, so you can query or modify it before the operation.
140
+
141
+ element.nestedFields({
142
+ beforeInsert: function(item) {
143
+ item.css('color', 'red'); // Make some operation
144
+ console.log(item + ' will be inserted.')
145
+ },
146
+ afterRemove: function(item) {
147
+ console.log(item + ' was removed.');
148
+ }
149
+ });
150
+
151
+ The before callbacks also allow you to control when the element will be inserted or removed, so you can perform async operations (ajax, of course!) or choose to not insert or remove the element at all if some condition is not met. Just receive a second parameter as the handler function.
152
+
153
+ element.nestedFields({
154
+ beforeInsert: function(item, insert) {
155
+ $.get('/ajax_function', function() {
156
+ insert();
94
157
  });
158
+ }
159
+ });
95
160
 
96
- Now enjoy your new nested model form!
161
+
162
+ ### Javascript API
163
+
164
+ It is possible to control nested fields programmatically using a jQuery-style API.
165
+
166
+ element.nestedFields('insert', function(item) {
167
+ // Make some operation with item
168
+ }, {skipBefore: true});
169
+
170
+ The code above inserts a new item and does not execute the `beforeInsert` callback function. The complete list of available methods is shown below.
171
+
172
+ * `insert(callback, options)` inserts a new item in the container. The `callback` function is executed just before the item is inserted. There are two available options: `skipBefore` and `skipAfter`. Both arguments are optional.
173
+ * `remove(element, options)` removes `element` from the container. There are two available options: `skipBefore` and `skipAfter`. The last argument is optional.
174
+ * `removeAll(options)` removes all elements from the container. There are two available options: `skipBefore` and `skipAfter`. The argument is optional.
175
+ * `items()` returns a list of items on the container.
176
+ * `destroy()` deactivates nested fields for the element.
177
+
178
+ These methods can be called from the element where nested fields are applied (e.g. a form) or from any element inside it (e.g. an input or the container itself).
97
179
 
98
180
 
99
181
  Compatibility
100
182
  -------------
101
183
 
102
- awesome_nested_fields works only with Rails 3.0 and Rails 3.1. Sorry, Rails 2.x users.
184
+ awesome_nested_fields works only with jQuery and Rails 3.x. Sorry, Rails 2.x users.
103
185
 
104
186
 
105
187
  TODO
@@ -108,8 +190,6 @@ TODO
108
190
  * Write tests
109
191
  * Write awesome demos
110
192
  * Make sure it can degrade gracefully
111
- * Return and API object on JS to make interaction easier
112
- * Make `nested_fields` call compatible with Rails `fields_for`
113
193
 
114
194
 
115
195
  Copyleft
@@ -7,3 +7,5 @@ module AwesomeNestedFields
7
7
 
8
8
  require 'awesome_nested_fields/version'
9
9
  end
10
+
11
+ require 'rails/form_helper'
@@ -1,3 +1,3 @@
1
1
  module AwesomeNestedFields
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -0,0 +1,34 @@
1
+ ActionView::Helpers::FormBuilder.class_eval do
2
+ def nested_fields_for(association, options={}, &block)
3
+ raise ArgumentError, 'Missing block to nested_fields_for' unless block_given?
4
+
5
+ options[:new_item_index] ||= 'new_nested_item'
6
+ options[:new_object] ||= self.object.class.reflect_on_association(association).klass.new
7
+ options[:item_template_class] ||= 'template item'
8
+ options[:empty_template_class] ||= 'template empty'
9
+ options[:show_empty] ||= false
10
+
11
+ output = @template.capture { fields_for(association, &block) }
12
+
13
+ if options[:show_empty] and self.object.send(association).empty?
14
+ output.safe_concat @template.capture { yield nil }
15
+ end
16
+
17
+ output.safe_concat nested_fields_templates(association, options, &block)
18
+
19
+ output
20
+ end
21
+
22
+ private
23
+ def nested_fields_templates(association, options, &block)
24
+ templates = @template.content_tag(:script, type: 'text/html', class: options[:item_template_class]) do
25
+ fields_for(association, options[:new_object], child_index: options[:new_item_index], &block)
26
+ end
27
+
28
+ if options[:show_empty]
29
+ templates.safe_concat @template.content_tag(:script, type: 'text/html', class: options[:empty_template_class], &block)
30
+ end
31
+
32
+ templates
33
+ end
34
+ end
@@ -13,13 +13,13 @@
13
13
  afterInsert: function(item) {},
14
14
  beforeRemove: function(item, callback) { callback() },
15
15
  afterRemove: function(item) {},
16
- itemTemplate: '.item.template',
17
- noneTemplate: '.none.template',
18
- container: '.container',
19
- item: '.item',
20
- none: '.none',
21
- add: '.add',
22
- remove: '.remove',
16
+ itemTemplateSelector: '.item.template',
17
+ emptyTemplateSelector: '.empty.template',
18
+ containerSelector: '.container',
19
+ itemSelector: '.item',
20
+ emptySelector: '.empty',
21
+ addSelector: '.add',
22
+ removeSelector: '.remove',
23
23
  newItemIndex: 'new_nested_item'
24
24
  };
25
25
 
@@ -36,35 +36,26 @@
36
36
  }
37
37
 
38
38
  options = $.extend({}, defaultSettings, options);
39
- options.itemTemplate = $(options.itemTemplate, $this);
40
- options.noneTemplate = $(options.noneTemplate, $this);
41
- options.container = $(options.container, $this);
42
- options.add = $(options.add, $this);
39
+ options.itemTemplate = $(options.itemTemplateSelector, $this);
40
+ options.emptyTemplate = $(options.emptyTemplateSelector, $this);
41
+ options.container = $(options.containerSelector, $this);
42
+ options.add = $(options.addSelector, $this);
43
43
  $this.data('nested-fields.options', options);
44
44
 
45
- options.add.bind('click.nested-fields', function(e) {
46
- e.preventDefault();
47
- var newItem = prepareTemplate(options);
48
- insertItemWithCallbacks(newItem, null, options);
49
- });
50
-
51
- $(options.item, options.container).each(function(i, item) {
52
- bindRemoveEvent(item, options);
53
- });
45
+ bindInsertToAdd(options);
46
+ bindRemoveToItems(options);
54
47
 
55
48
  return $this;
56
49
  },
57
50
 
58
51
  insert: function(callback, options) {
59
52
  options = $.extend({}, getOptions(this), options);
60
- var newItem = prepareTemplate(options);
61
-
62
- insertItemWithCallbacks(newItem, callback, options);
53
+ return insertItemWithCallbacks(callback, options);
63
54
  },
64
55
 
65
56
  remove: function(element, options) {
66
57
  options = $.extend({}, getOptions(this), options);
67
- return removeItem(element, options);
58
+ return removeItemWithCallbacks(element, options);
68
59
  },
69
60
 
70
61
  removeAll: function(options) {
@@ -94,6 +85,8 @@
94
85
  }
95
86
  };
96
87
 
88
+ // Initialization functions
89
+
97
90
  function getOptions(element) {
98
91
  element = $(element);
99
92
  while(element.length > 0) {
@@ -107,6 +100,21 @@
107
100
  return null;
108
101
  }
109
102
 
103
+ function bindInsertToAdd(options) {
104
+ options.add.bind('click.nested-fields', function(e) {
105
+ e.preventDefault();
106
+ insertItemWithCallbacks(null, options);
107
+ });
108
+ }
109
+
110
+ function bindRemoveToItems(options) {
111
+ $(options.itemSelector, options.containerSelector).each(function(i, item) {
112
+ bindRemoveToItem(item, options);
113
+ });
114
+ }
115
+
116
+ // Insertion functions
117
+
110
118
  function prepareTemplate(options) {
111
119
  var regexp = new RegExp(options.newItemIndex, 'g');
112
120
  var newId = new Date().getTime();
@@ -116,26 +124,27 @@
116
124
  newItem.attr('data-new-record', true);
117
125
  newItem.attr('data-record-id', newId);
118
126
 
119
- bindRemoveEvent(newItem, options);
127
+ bindRemoveToItem(newItem, options);
120
128
 
121
129
  return newItem;
122
130
  }
123
131
 
124
- function insertItem(newItem, options) {
125
- removeNone(options);
126
- options.container.append(newItem);
127
- }
128
-
129
- function insertItemWithCallbacks(newItem, onInsertCallback, options) {
132
+ function insertItemWithCallbacks(onInsertCallback, options) {
133
+ var newItem = prepareTemplate(options);
134
+
130
135
  function insert() {
131
136
  if(onInsertCallback) {
132
137
  onInsertCallback(newItem);
133
138
  }
134
- insertItem(newItem, options);
139
+ removeEmpty(options);
140
+ options.container.append(newItem);
135
141
  }
136
142
 
137
- if(!options.skipBefore) {
143
+ if(!options.skipBefore) {
138
144
  options.beforeInsert(newItem, insert);
145
+ if(options.beforeInsert.length <= 1) {
146
+ insert();
147
+ }
139
148
  } else {
140
149
  insert();
141
150
  }
@@ -147,7 +156,13 @@
147
156
  return newItem;
148
157
  }
149
158
 
150
- function removeItem(element, options) {
159
+ function removeEmpty(options) {
160
+ findEmpty(options).remove();
161
+ }
162
+
163
+ // Removal functions
164
+
165
+ function removeItemWithCallbacks(element, options) {
151
166
  function remove() {
152
167
  if($element.attr('data-new-record')) { // record is new
153
168
  $element.remove();
@@ -155,12 +170,15 @@
155
170
  $element.find("INPUT[name$='[_destroy]']").val('true');
156
171
  $element.hide();
157
172
  }
158
- insertNone(options);
173
+ insertEmpty(options);
159
174
  }
160
175
 
161
176
  var $element = $(element);
162
177
  if(!options.skipBefore) {
163
178
  options.beforeRemove($element, remove);
179
+ if(options.beforeRemove.length <= 1) {
180
+ insert();
181
+ }
164
182
  } else {
165
183
  remove();
166
184
  }
@@ -172,35 +190,33 @@
172
190
  return $element;
173
191
  }
174
192
 
175
- function bindRemoveEvent(item, options) {
176
- var removeHandler = $(item).find(options.remove);
193
+ function insertEmpty(options) {
194
+ if(findItems(options).length === 0) {
195
+ options.container.append(options.emptyTemplate.html());
196
+ }
197
+ }
198
+
199
+ function bindRemoveToItem(item, options) {
200
+ var removeHandler = $(item).find(options.removeSelector);
177
201
  var needsConfirmation = removeHandler.attr('data-confirm');
178
202
 
179
203
  var event = needsConfirmation ? 'confirm:complete' : 'click';
180
204
  removeHandler.bind(event + '.nested-fields', function(e, confirmed) {
181
205
  e.preventDefault();
182
206
  if(confirmed === undefined || confirmed === true) {
183
- removeItem(item, options);
207
+ removeItemWithCallbacks(item, options);
184
208
  }
185
209
  });
186
210
  }
187
211
 
188
- function insertNone(options) {
189
- if(findItems(options).length === 0) {
190
- options.container.append(options.noneTemplate.html());
191
- }
192
- }
193
-
194
- function removeNone(options) {
195
- findNone(options).remove();
196
- }
212
+ // Find functions
197
213
 
198
214
  function findItems(options) {
199
- return options.container.find(options.item + ':visible');
215
+ return options.container.find(options.itemSelector + ':visible');
200
216
  }
201
217
 
202
- function findNone(options) {
203
- return options.container.find(options.none);
218
+ function findEmpty(options) {
219
+ return options.container.find(options.emptySelector);
204
220
  }
205
221
 
206
222
  })(jQuery);
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awesome_nested_fields
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-03 00:00:00.000000000Z
12
+ date: 2011-07-11 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
- requirement: &2156703520 !ruby/object:Gem::Requirement
16
+ requirement: &2155985320 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.0.0
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2156703520
24
+ version_requirements: *2155985320
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rails
27
- requirement: &2156703060 !ruby/object:Gem::Requirement
27
+ requirement: &2155984440 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: 3.0.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *2156703060
35
+ version_requirements: *2155984440
36
36
  description: Awesome dynamic nested fields for Rails and jQuery
37
37
  email: lailson@guava.com.br
38
38
  executables: []
@@ -45,13 +45,13 @@ files:
45
45
  - LICENSE
46
46
  - README.md
47
47
  - Rakefile
48
- - app/helpers/awesome_nested_fields_helper.rb
49
48
  - awesome_nested_fields.gemspec
50
49
  - lib/awesome_nested_fields.rb
51
50
  - lib/awesome_nested_fields/engine.rb
52
51
  - lib/awesome_nested_fields/railtie.rb
53
52
  - lib/awesome_nested_fields/version.rb
54
53
  - lib/generators/awesome_nested_fields/install/install_generator.rb
54
+ - lib/rails/form_helper.rb
55
55
  - vendor/assets/javascripts/jquery.nested-fields.js
56
56
  homepage: http://rubygems.org/gems/awesome_nested_fields
57
57
  licenses: []
@@ -1,53 +0,0 @@
1
- module AwesomeNestedFieldsHelper
2
-
3
- def nested_fields(builder, association, options={})
4
- nested_fields_items(builder, association, options) <<
5
- nested_fields_template(builder, association, options)
6
- end
7
-
8
- def nested_fields_items(builder, association, options={})
9
- options = nested_fields_process_default_options(options, builder, association)
10
-
11
- items = ''
12
- builder.fields_for(association) do |f|
13
- items << render(options[:partial], options[:builder_local] => f)
14
- end
15
-
16
- if options[:none_partial] and builder.object.send(association).empty?
17
- items << render(options[:none_partial])
18
- end
19
-
20
- items.html_safe
21
- end
22
-
23
- def nested_fields_template(builder, association, options={})
24
- options = nested_fields_process_default_options(options, builder, association)
25
-
26
- templates = content_tag(:script, type: 'text/html', class: options[:item_template_class]) do
27
- builder.fields_for(association, options[:new_object], child_index: options[:new_item_index]) do |f|
28
- render(options[:partial], options[:builder_local] => f)
29
- end
30
- end
31
-
32
- if options[:none_partial]
33
- templates << content_tag(:script, type: 'text/html', class: options[:none_template_class]) do
34
- builder.fields_for(association, options[:new_object], child_index: options[:new_item_index]) do |f|
35
- render(options[:none_partial], options[:builder_local] => f)
36
- end
37
- end
38
- end
39
-
40
- templates.html_safe
41
- end
42
-
43
- protected
44
- def nested_fields_process_default_options(options, builder, association)
45
- options[:new_object] ||= builder.object.class.reflect_on_association(association).klass.new
46
- options[:partial] ||= association.to_s.singularize
47
- options[:builder_local] ||= :f
48
- options[:item_template_class] ||= 'template item'
49
- options[:none_template_class] ||= 'template none'
50
- options[:new_item_index] ||= 'new_nested_item'
51
- options
52
- end
53
- end