abyme 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54da5a9a0118b560b3e9006d85f175d3a84ccd067a8654a61c4a46dfeb5ba15b
4
- data.tar.gz: 3d836ed9cf5564e2b311a8e100466bc589307c0c5bf77f527fae70c94bc05fb9
3
+ metadata.gz: 0aefabdd57f0839a84d951c5cf2c1cbf8e515b9e6735ee4786cd867a81ef6817
4
+ data.tar.gz: 7a54056dd25d3dd5783904ee2b54891040c3c614daa6e667c2eca4108e3e1aef
5
5
  SHA512:
6
- metadata.gz: e7095310324451424bccce250acd5cddd059108861581169990ec1be1abe8e5a9453aaf7710f1cb62d63bebbb5af4220f6257897119d43477e2a923b01067d3e
7
- data.tar.gz: 20bb396ba92d7b739fdf137d37627163f71f3dbc4dbbe84ea346d24ccbe7317af2b253a1b451e52a650da0e01b078a153e8565e268475765769347e2c1e56be5
6
+ metadata.gz: 77880b3666f91e2c0f525ac89c25c74d98a12b89714068b3834e236d806f59b264679a24eb78f962f4f37899146de5f225664633d4ed1470a3827a2441e40a97
7
+ data.tar.gz: 79f862ec2e13b4e491b7109a691742f8f986f1651c779ba2b8f56d283185a35116edc632b96df28347e772e56058b9321279e762a35915641e0d85c00306e640
Binary file
data/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  abyme is a modern take on handling dynamic nested forms in Rails 6+ using StimulusJS.
4
4
 
5
+ ## Disclaimer
6
+ This project is still a work in progress and subject to change. We encourage not to use it in production code just yet.
7
+
8
+ Any enhancement proposition or bug report welcome !
9
+
10
+ General remarks :
11
+ * A demo app will soon be online.
12
+ * For now, the gem is tested through our demo app. Specific autonomous tests will be transfered/written in the following days.
13
+ * Help is very much wanted on the Events part of the gem (see bottom of this documentation)
14
+
5
15
  ## Installation
6
16
 
7
17
  Add this line to your application's Gemfile:
@@ -46,14 +56,14 @@ Let's consider a to-do application with Projects having many Taks, themselves ha
46
56
  ```ruby
47
57
  # models/project.rb
48
58
  class Project < ApplicationRecord
49
- has_many :tasks, inverse_of: :project, dependent: :destroy
59
+ has_many :tasks
50
60
  validates :title, :description, presence: true
51
61
  end
52
62
 
53
63
  # models/task.rb
54
64
  class Task < ApplicationRecord
55
65
  belongs_to :project
56
- has_many :comments, inverse_of: :project, dependent: :destroy
66
+ has_many :comments
57
67
  validates :title, :description, presence: true
58
68
  end
59
69
 
@@ -63,50 +73,257 @@ class Comment < ApplicationRecord
63
73
  validates :content, presence: true
64
74
  end
65
75
  ```
66
- The end-goal is to be able to create a project along with different tasks, and immediately add comments to some of these tasks ; all in a single form.
67
- What we'll have is a 2-level nested form. Thus, we'll need to add these lines to both `Project` and `Task` :
76
+ The end-goal here is to be able to create a project along with different tasks, and immediately add comments to some of these tasks ; all within a single form.
77
+ What we'll have is a 2-level nested form. Thus, we'll need to configure our `Project` and `Task` models like so :
68
78
  ```ruby
69
79
  # models/project.rb
70
80
  class Project < ApplicationRecord
71
81
  include Abyme::Model
72
- #...
82
+ has_many :tasks, inverse_of: :project
83
+ # ...
73
84
  abyme_for :tasks
74
85
  end
75
86
 
76
87
  # models/task.rb
77
88
  class Task < ApplicationRecord
78
89
  include Abyme::Model
79
- #...
90
+ has_many :comments, inverse_of: :task
91
+ # ...
80
92
  abyme_for :comments
81
93
  end
82
94
  ```
95
+ Note the use of the `inverse_of` option. It is needed for Rails to effectively associate children to their yet unsaved parent. Have a peek to the bottom of [this page](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for) for more info.
83
96
 
84
97
  ### Controller
85
98
  Since we're dealing with one form, we're only concerned with one controller : the one the form routes to. In our example, this would be the `ProjectsController`.
86
- The only configuration needed here will be our strong_params. Nested attributes require a very specific syntax to white-list the permitted attributes. It looks like this :
99
+ The only configuration needed here will concern our strong params. Nested attributes require a very specific syntax to white-list the permitted attributes. It looks like this :
87
100
 
88
101
  ```ruby
89
- def project_params
90
- params.require(:project).permit(
91
- :title, :description, tasks_attributes: [
92
- :id, :title, :description, :_destroy, comments_attributes: [
93
- :id, :content, :_destroy
94
- ]
102
+ def project_params
103
+ params.require(:project).permit(
104
+ :title, :description, tasks_attributes: [
105
+ :id, :title, :description, :_destroy, comments_attributes: [
106
+ :id, :content, :_destroy
95
107
  ]
96
- )
97
- end
108
+ ]
109
+ )
110
+ end
98
111
  ```
99
112
  A few explanations here.
100
113
 
101
114
  * To permit a nested model attributes in your params, you'll need to pass the `association_attributes: [...]` hash at the end of your resource attributes. Key will always be `association_name` followed by `_attributes`, while the value will be an array of symbolized attributes, just like usual.
102
115
 
103
- **Note**: if your association is a singular one (`has_one` or `belongs_to`, the association will be singular ; if a Project `has_one :owner`, you would then need to pass `owner_attributes: [...]`)
116
+ > **Note**: if your association is a singular one (`has_one` or `belongs_to`) the association will be singular ; if a Project `has_one :owner`, you would then need to pass `owner_attributes: [...]`)
117
+
118
+ * You may have remarked the presence of `id` and `_destroy` among those params. These are necessary for edit actions : if you want to allow your users to destroy or update existing records, these are **mandatory**. Otherwise, Rails won't be able to recognize these records as existing ones, and will just create new ones. More info [here](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html).
119
+
120
+ ## Basic Usage
121
+
122
+ Dealing with nested attributes means you'll generally have to handle a few things inside your form:
123
+ * Display fields for the **persisted records** (here, already existing `:tasks`)
124
+ * Display fields for the **new records** (future `:tasks` not yet persisted)
125
+ * A button to **trigger the addition** of fields for a new resource (an `Add a new task` button)
126
+ * A button to **remove fields** for a given resource (`Remove task`)
127
+
128
+ abyme provides helper methods for all these. Here's how our form for `Project` looks like when using default values:
129
+
130
+ ```ruby
131
+ # views/projects/_form.html.erb
132
+ <%= simple_form_for @project do |f| %>
133
+ <%= f.input :title %>
134
+ <%= f.input :description %>
135
+ <%= f.submit 'Save' %>
136
+
137
+ <%= abymize(:tasks, f) do |abyme| %>
138
+ <%= abyme.records %>
139
+ <%= abyme.new_records %>
140
+ <%= add_association %>
141
+ <% end %>
142
+ <% end %>
143
+ ```
144
+
145
+ `abyme.records` will contain the persisted associations fields, while `abyme.new_records` will contain fields for the new associations. `add_association` will by default generate a button with a text of type "Add `resource_name`". To work properly, this method **has** to be called **inside the block** passed to the `abymize` method.
146
+
147
+ Now where's the code for these fields ? abyme will assume a **partial** to be present in the directory `/views/abyme` with a *name respecting this naming convention* (just like with [cocoon](https://github.com/nathanvda/cocoon#basic-usage)): `_singular_association_name_fields.html.erb`.
148
+
149
+ This partial might look like this:
150
+ ```ruby
151
+ # views/abyme/_task_fields.html.erb
152
+ <%= f.input :title %>
153
+ <%= f.input :description %>
154
+ <%= f.hidden_field :_destroy %>
155
+
156
+ <%= remove_association(tag: :div) do %>
157
+ <i class="fas fa-trash"></i>
158
+ <% end %>
159
+ ```
160
+
161
+ Note the presence of the `remove_association` button. Here, we pass it an option to make it a `<div>`, as well as a block to customize its content. Don't forget the `_destroy` attribute, needed to mark items for destruction.
162
+
163
+ ### What about the controller ?
164
+
165
+ What about it ? Well, not much. That's the actual magical thing about `nested_attributes`: once your model is aware of its acceptance of those for a given association, and your strong params are correctly configured, there's nothing else to do.
166
+ `@project.create(project_params)` is all you'll need to save a project along with its descendants 👨‍👧‍👧
167
+
168
+ ### Auto mode
169
+
170
+ Let's now take care of our comments fields. We'll add these using our neat *automatic mode*: just stick this line at the end of the partial:
171
+ ```ruby
172
+ # views/abyme/_task_fields.html.erb
173
+ # ... rest of the partial above
174
+ <%= abymize(:comments, f) %>
175
+ ```
176
+ Where's the rest of the code ? Well, if the default configuration you saw above in the `_form.html.erb` suits you, and the order in which the different resources appear feels right (persisted first, new fields second, and the 'Add' button last), then you can just spare the block, and it will be taken care of for you. We'll just write our `_comment_fields.html.erb` partial in the `views/abyme` directory and we'll be all set.
177
+
178
+ ## Advanced usage
179
+ ### Models
180
+ In models, the `abyme_for :association` acts as an alias for this command :
181
+
182
+ ```ruby
183
+ accepts_nested_attributes_for :association, reject_if: :all_blank, :allow_destroy: true
184
+ ```
185
+
186
+ Which is the way you would configure `nested_attributes` 90% of the time. Should you want to pass [any available options](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for) to this method or change those, you may just pass them as with the original method :
187
+ ```ruby
188
+ abyme_for :association, limit: 3, allow_destroy: false
189
+ ```
190
+
191
+ ### Views
104
192
 
105
- * You may have remarked the presence of `id` and `_destroy` among those params. These are necessary for edit actions : if you want to allow your users to destroy or update existing records, these are **mandatory**. Otherwise, Rails won't be able to recognize these records as existing ones, and will just create new ones.
193
+ #### #records
194
+ A few options can be passed to `abyme.records`:
195
+ * `collection:` : allows you to pass a collection of your choice to only display specific objects.
196
+ ```ruby
197
+ <%= abymize(:tasks, f) do |abyme| %>
198
+ <%= abyme.records(collection: @project.tasks.where(done: false)) %>
199
+ <%= abyme.new_records %>
200
+ <%= add_association %>
201
+ <% end %>
202
+ ```
203
+ * `order:` : allows you to pass an ActiveRecord `order` method to sort your instances the way you want.
204
+ ```ruby
205
+ <%= abymize(:tasks, f) do |abyme| %>
206
+ <%= abyme.records(order: { created_at: :asc }) %>
207
+ <%= abyme.new_records %>
208
+ <%= add_association %>
209
+ <% end %>
210
+ ```
211
+ * `partial:` : allows you to indicate a custom partial, if one has not already been passed to `abymize`.
212
+ ```ruby
213
+ <%= abymize(:tasks, f) do |abyme| %>
214
+ <%= abyme.records %>
215
+ <%= abyme.new_records(partial: 'projects/task_fields') %>
216
+ <%= add_association %>
217
+ <% end %>
218
+ ```
219
+ * `fields_html:` : gives you the possibility to add any HTML attribute you may want to each set of fields. By default, an `abyme--fields` and an `singular_association-fields` class are already present.
220
+ ```ruby
221
+ <%= abymize(:tasks, f) do |abyme| %>
222
+ <%= abyme.records(fields_html: { class: "some-class" }) %>
223
+ # Every set of persisted fields will have these 3 classes : 'abyme--fields', 'task-fields', and 'some-class'
224
+ <%= abyme.new_records %>
225
+ <%= add_association %>
226
+ <% end %>
227
+ ```
228
+ * `wrapper_html:` : gives you the possibility to add any HTML attribute you may want to the wrapper containing all fields. By default, an `abyme-association-wrapper` class is already present.
229
+ ```ruby
230
+ <%= abymize(:tasks, f) do |abyme| %>
231
+ <%= abyme.records(html: { class: "persisted-records" }) %>
232
+ # The wrapper containing all persisted task fields will have an id "abyme-tasks-wrapper" and a class "persisted-records"
233
+ <%= abyme.new_records %>
234
+ <%= add_association %>
235
+ <% end %>
236
+ ```
237
+ #### #new_records
238
+ Here are the options that can be passed to `abyme.new_records`:
239
+ * `position:` : allows you to specify whether new fields added dynamically should go at the top or at the bottom. `:end` is the default value.
240
+ ```ruby
241
+ <%= abymize(:tasks, f) do |abyme| %>
242
+ <%= abyme.records %>
243
+ <%= abyme.new_records(position: :start) %>
244
+ <%= add_association %>
245
+ <% end %>
246
+ ```
247
+ * `partial:` : same as `#records`
248
+ * `fields_html:` : same as `#records`
249
+ * `wrapper_html:` : same as `#records`
250
+
251
+ #### #add_association, #remove_association
252
+ These 2 methods behave the same. Here are their options :
253
+ * `tag:` : allows you to specify a tag of your choosing, like `:a`, or `:div`. Default is `:button`.
254
+ * `content:` : the text to display inside the element. Default is `Add association_name`
255
+ * `html:` : gives you the possibility to add any HTML attribute you may want to the element.
256
+ ```ruby
257
+ <%= abymize(:tasks, f) do |abyme| %>
258
+ # ...
259
+ <%= add_association(tag: :a, content: "Add a super task", html: {id: "add-super-task"}) %>
260
+ <% end %>
261
+ ```
262
+
263
+ As you may have seen above, you can also pass a block to the method to give it whatever HTML content you want :
264
+ ```ruby
265
+ <%= abymize(:tasks, f) do |abyme| %>
266
+ # ...
267
+ <%= add_association(tag: :div, html: {id: "add-super-task", class: "flex"}) do %>
268
+ <i class="fas fa-plus"></i>
269
+ <h2>Add a super task</h2>
270
+ <% end %>
271
+ <% end %>
272
+ ```
273
+
274
+
275
+ #### #abymize(:association, form_object)
276
+ This is the container for all your nested fields. It takes two parameters (the symbolized association and the `form_builder`), and some optional ones. Please note an id is automatically added to this element, which value is : `abyme--association`.
277
+ * `partial:` : allows you to indicate a custom partial path for both `records` and `new_records`
278
+ ```ruby
279
+ <%= abymize(:tasks, f, partial: 'projects/task_fields') do |abyme| %>
280
+ <%= abyme.records %>
281
+ <%= abyme.new_records %>
282
+ <%= add_association %>
283
+ <% end %>
284
+ ```
285
+ * `limit:` : allows you to limit the number of fields that can be created through JS. If you need to limit the number of associations in database, you will need to add validations. You can also pass an option [in your model as well](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for).
286
+ ```ruby
287
+ <%= abymize(:tasks, f, limit: 5) do |abyme| %>
288
+ # Beyond 5 tasks, the add button won't add any more fields. See events section below to see how to handle the 'abyme:limit-reached' event
289
+ <%= abyme.records %>
290
+ <%= abyme.new_records %>
291
+ <%= add_association %>
292
+ <% end %>
293
+ ```
294
+ * `min-count` : by default, there won't be any blank fields added on page load. By passing a `min-count` option, you can set how many empty fields should appear in the form.
295
+ ```ruby
296
+ <%= abymize(:tasks, f, min-count: 1) do |abyme| %>
297
+ # 1 blank task will automatically be added to the form.
298
+ <%= abyme.records %>
299
+ <%= abyme.new_records %>
300
+ <%= add_association %>
301
+ <% end %>
302
+ ```
303
+
304
+ *When in auto mode*, the abymize method can take a few options:
305
+ * `add-button-text:` : this will set the `add_association` button text to the string of your choice.
306
+ * All options that should be passed to either `records` or `new_records` can be passed here and will be passed down.
307
+
308
+ ## Events
309
+ This part is still a work in progress and subject to change. We're providing some basic self-explanatory events to attach to. These are emitted by the main container (created by the `abymize` method).
310
+
311
+ We're currently thinking about a way to attach to these via Stimulus. Coming soon !
106
312
 
107
- ### View
313
+ ### Lifecycle events
314
+ * `abyme:before-add`
315
+ * `abyme:after-add`
316
+ * `abyme:before-remove`
317
+ * `abyme:after-remove`
318
+ ```javascript
319
+ document.getElementById('abyme--tasks').addEventListener('abyme:before-add', yourCallback)
320
+ ```
108
321
 
109
- TODO...
322
+ ### Other events
323
+ * `abyme:limit-reached`
324
+ ```javascript
325
+ document.getElementById('abyme--tasks').addEventListener('abyme:limit-reached', () => { alert('You reached the max number of tasks !) })
326
+ ```
110
327
 
111
328
  ## Development
112
329
 
Binary file
@@ -1,60 +1,60 @@
1
1
  import { Controller } from 'stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ['template', 'associations'];
4
+ static targets = ['template', 'associations', 'fields', 'newFields'];
5
5
 
6
6
  connect() {
7
- console.log('Abyme Connect');
7
+ if (this.count) {
8
+ this.addDefaultAssociations();
9
+ }
8
10
  }
9
11
 
12
+ get count() {
13
+ return this.element.dataset.minCount || 0;
14
+ }
15
+
10
16
  get position() {
11
17
  return this.associationsTarget.dataset.abymePosition === 'end' ? 'beforeend' : 'afterbegin';
12
18
  }
13
19
 
14
20
  add_association(event) {
15
- event.preventDefault();
16
-
17
- let html = this.templateTarget.innerHTML.replace(
18
- /NEW_RECORD/g,
19
- new Date().getTime()
20
- );
21
-
22
- if (html.match(/<template[\s\S]+<\/template>/)) {
23
- const template = html
24
- .match(/<template[\s\S]+<\/template>/)[0]
25
- .replace(/(\[\d{12,}\])(\[[^\[\]]+\]"){1}/g, `[NEW_RECORD]$2`);
26
-
27
- html = html.replace(/<template[\s\S]+<\/template>/g, template);
21
+ if (event) {
22
+ event.preventDefault();
23
+ }
24
+ // check for limit reached
25
+ if (this.element.dataset.limit && this.limit_check()) {
26
+ this.create_event('limit-reached')
27
+ return false
28
28
  }
29
29
 
30
- this.create_event('before-add', html)
30
+ const html = this.build_html();
31
+ this.create_event('before-add');
31
32
  this.associationsTarget.insertAdjacentHTML(this.position, html);
32
- this.create_event('after-add', html)
33
+ this.create_event('after-add');
33
34
  }
34
35
 
35
36
  remove_association(event) {
36
37
  event.preventDefault();
37
-
38
- this.create_event('before-remove')
39
- let wrapper = event.target.closest('.abyme--fields');
40
- wrapper.querySelector("input[name*='_destroy']").value = 1;
41
- wrapper.style.display = 'none';
42
- this.create_event('after-remove')
38
+ this.create_event('before-remove');
39
+ this.mark_for_destroy(event);
40
+ this.create_event('after-remove');
43
41
  }
44
42
 
43
+ // LIFECYCLE EVENTS RELATED
44
+
45
45
  create_event(stage, html = null) {
46
- const event = new CustomEvent(`abyme:${stage}`, { detail: {controller: this, content: html} })
47
- this.element.dispatchEvent(event)
46
+ const event = new CustomEvent(`abyme:${stage}`, { detail: {controller: this, content: html} });
47
+ this.element.dispatchEvent(event);
48
48
  // WIP
49
- this.dispatch(event, stage)
49
+ this.dispatch(event, stage);
50
50
  }
51
51
 
52
52
  // WIP : Trying to integrate event handling through controller inheritance
53
53
  dispatch(event, stage) {
54
- if (stage === 'before-add' && this.abymeBeforeAdd) this.abymeBeforeAdd(event)
55
- if (stage === 'after-add' && this.abymeAfterAdd) this.abymeAfterAdd(event)
56
- if (stage === 'before-remove' && this.abymeBeforeRemove) this.abymeBeforeAdd(event)
57
- if (stage === 'after-remove' && this.abymeAfterRemove) this.abymeAfterRemove(event)
54
+ if (stage === 'before-add' && this.abymeBeforeAdd) this.abymeBeforeAdd(event);
55
+ if (stage === 'after-add' && this.abymeAfterAdd) this.abymeAfterAdd(event);
56
+ if (stage === 'before-remove' && this.abymeBeforeRemove) this.abymeBeforeAdd(event);
57
+ if (stage === 'after-remove' && this.abymeAfterRemove) this.abymeAfterRemove(event);
58
58
  }
59
59
 
60
60
  abymeBeforeAdd(event) {
@@ -68,4 +68,54 @@ export default class extends Controller {
68
68
 
69
69
  abymeAfterRemove(event) {
70
70
  }
71
+
72
+ // UTILITIES
73
+
74
+ // build html
75
+ build_html() {
76
+ let html = this.templateTarget.innerHTML.replace(
77
+ /NEW_RECORD/g,
78
+ new Date().getTime()
79
+ );
80
+
81
+ if (html.match(/<template[\s\S]+<\/template>/)) {
82
+ const template = html
83
+ .match(/<template[\s\S]+<\/template>/)[0]
84
+ .replace(/(\[\d{12,}\])(\[[^\[\]]+\]"){1}/g, `[NEW_RECORD]$2`);
85
+
86
+ html = html.replace(/<template[\s\S]+<\/template>/g, template);
87
+ }
88
+
89
+ return html;
90
+ }
91
+
92
+ // mark association for destroy
93
+ mark_for_destroy(event) {
94
+ let item = event.target.closest('.abyme--fields');
95
+ item.querySelector("input[name*='_destroy']").value = 1;
96
+ item.style.display = 'none';
97
+ item.classList.add('abyme--marked-for-destroy')
98
+ }
99
+
100
+ // check if associations limit is reached
101
+ limit_check() {
102
+ return (this.newFieldsTargets
103
+ .filter(item => !item.classList.contains('abyme--marked-for-destroy'))).length
104
+ >= parseInt(this.element.dataset.limit)
105
+ }
106
+
107
+ // Add default blank associations at page load
108
+ async addDefaultAssociations() {
109
+ let i = 0
110
+ while (i < this.count) {
111
+ this.add_association()
112
+ i++
113
+ // Sleep function to ensure uniqueness of timestamp
114
+ await this.sleep(1);
115
+ }
116
+ }
117
+
118
+ sleep(ms) {
119
+ return new Promise(resolve => setTimeout(resolve, ms));
120
+ }
71
121
  }
Binary file
@@ -2,60 +2,31 @@ module Abyme
2
2
  class AbymeBuilder < ActionView::Base
3
3
  include ActionView
4
4
 
5
- def initialize(association:, form:, lookup_context:, &block)
5
+ def initialize(association:, form:, lookup_context:, partial:, &block)
6
6
  @association = association
7
7
  @form = form
8
8
  @lookup_context = lookup_context
9
+ @partial = partial
9
10
  yield(self) if block_given?
10
11
  end
11
12
 
12
13
  def records(options = {})
13
- persisted_records_for(@association, @form, options) do |form|
14
- render_association_partial(form, options)
14
+ persisted_records_for(@association, @form, options) do |fields_for_association|
15
+ render_association_partial(fields_for_association, options)
15
16
  end
16
17
  end
17
18
 
18
19
  def new_records(options = {}, &block)
19
- new_records_for(@association, @form, options) do |form|
20
- render_association_partial(form, options)
20
+ new_records_for(@association, @form, options) do |fields_for_association|
21
+ render_association_partial(fields_for_association, options)
21
22
  end
22
23
  end
23
24
 
24
- # def add_association(options = {}, &block)
25
- # action = 'click->abyme#add_association'
26
- # create_button(action, options, &block)
27
- # end
28
-
29
- # def remove_association(options = {}, &block)
30
- # action = 'click->abyme#remove_association'
31
- # create_button(action, options, &block)
32
- # end
33
-
34
25
  private
35
26
 
36
- def render_association_partial(form, options)
37
- partial = options[:partial] || "shared/#{@association.to_s.singularize}_fields"
38
- # ActionController::Base.render(partial: "shared/#{@association.to_s.singularize}_fields", locals: { f: form })
39
- ActionController::Base.render(partial: partial, locals: { f: form })
27
+ def render_association_partial(fields, options)
28
+ partial = @partial || options[:partial] || "abyme/#{@association.to_s.singularize}_fields"
29
+ ActionController::Base.render(partial: partial, locals: { f: fields })
40
30
  end
41
-
42
- # def create_button(action, options, &block)
43
- # options[:attributes] = {} if options[:attributes].nil?
44
- # options[:tag] = :button if options[:tag].nil?
45
-
46
- # if block_given?
47
- # concat content_tag(options[:tag], { data: { action: action }}.merge(options[:attributes])) do
48
- # # capture(&block)
49
- # yield
50
- # end
51
- # else
52
- # render content_tag(options[:tag], options[:content], {data: { action: action }}.merge(options[:attributes]))
53
- # end
54
- # end
55
-
56
- # def formatize(association)
57
- # association.class.name.tableize
58
- # end
59
-
60
31
  end
61
32
  end
@@ -1,8 +1,8 @@
1
1
  module Abyme
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 1
5
- PATCH = 3
4
+ MINOR = 2
5
+ PATCH = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, PATCH].join(".")
8
8
  end
@@ -1,32 +1,41 @@
1
- require 'abyme/abyme_builder'
2
-
3
1
  module Abyme
4
2
  module ViewHelpers
5
3
 
6
4
  def abymize(association, form, options = {}, &block)
7
- content_tag(:div, data: { controller: 'abyme' }, id: "abyme--#{association}") do
5
+ content_tag(:div, data: { controller: 'abyme', limit: options[:limit], min_count: options[:min_count] }, id: "abyme--#{association}") do
8
6
  if block_given?
9
- yield(Abyme::AbymeBuilder.new(association: association, form: form, lookup_context: self.lookup_context))
7
+ yield(Abyme::AbymeBuilder.new(
8
+ association: association, form: form, lookup_context: self.lookup_context, partial: options[:partial]
9
+ )
10
+ )
10
11
  else
11
12
  model = association.to_s.singularize.classify.constantize
12
13
  concat(persisted_records_for(association, form, options))
13
14
  concat(new_records_for(association, form, options))
14
- concat(add_association(content: options[:add] || "Add #{model}"))
15
+ concat(add_association(content: options[:button_text] || "Add #{model}"))
15
16
  end
16
17
  end
17
18
  end
18
19
 
19
20
  def new_records_for(association, form, options = {}, &block)
20
- content_tag(:div, data: { target: 'abyme.associations', association: association, abyme_position: options[:position] || :end }) do
21
+ options[:wrapper_html] ||= {}
22
+
23
+ wrapper_default = {
24
+ data: {
25
+ target: 'abyme.associations',
26
+ association: association,
27
+ abyme_position: options[:position] || :end
28
+ }
29
+ }
30
+
31
+ fields_default = { data: { target: 'abyme.fields abyme.newFields' } }
32
+
33
+ content_tag(:div, build_attributes(wrapper_default, options[:wrapper_html])) do
21
34
  content_tag(:template, class: "abyme--#{association.to_s.singularize}_template", data: { target: 'abyme.template' }) do
22
35
  form.fields_for association, association.to_s.classify.constantize.new, child_index: 'NEW_RECORD' do |f|
23
- content_tag(:div, basic_markup(options[:html])) do
24
- if block_given?
25
- # Here, f is the fields_for ; f.object becomes association.new rather than the original form.object
26
- yield(f)
27
- else
28
- render "shared/#{association.to_s.singularize}_fields", f: f
29
- end
36
+ content_tag(:div, build_attributes(fields_default, basic_fields_markup(options[:fields_html], association))) do
37
+ # Here, if a block is passed, we're passing the association fields to it, rather than the form itself
38
+ block_given? ? yield(f) : render("abyme/#{association.to_s.singularize}_fields", f: f)
30
39
  end
31
40
  end
32
41
  end
@@ -34,29 +43,21 @@ module Abyme
34
43
  end
35
44
 
36
45
  def persisted_records_for(association, form, options = {})
37
- if options[:collection]
38
- records = options[:collection]
39
- else
40
- records = form.object.send(association)
41
- end
46
+ records = options[:collection] || form.object.send(association)
47
+ options[:wrapper_html] ||= {}
48
+ fields_default = { data: { target: 'abyme.fields' } }
42
49
 
43
50
  if options[:order].present?
44
51
  records = records.order(options[:order])
45
-
46
- # GET INVALID RECORDS
52
+ # Get invalid records
47
53
  invalids = form.object.send(association).reject(&:persisted?)
48
-
49
- if invalids.any?
50
- records = records.to_a.concat(invalids)
51
- end
52
- end
53
-
54
- form.fields_for(association, records) do |f|
55
- content_tag(:div, basic_markup(options[:html])) do
56
- if block_given?
57
- yield(f)
58
- else
59
- render "shared/#{association.to_s.singularize}_fields", f: f
54
+ records = records.to_a.concat(invalids) if invalids.any?
55
+ end
56
+
57
+ content_tag(:div, options[:wrapper_html]) do
58
+ form.fields_for(association, records) do |f|
59
+ content_tag(:div, build_attributes(fields_default, basic_fields_markup(options[:fields_html], association))) do
60
+ block_given? ? yield(f) : render("abyme/#{association.to_s.singularize}_fields", f: f)
60
61
  end
61
62
  end
62
63
  end
@@ -88,17 +89,27 @@ module Abyme
88
89
  end
89
90
  end
90
91
 
91
- def basic_markup(html)
92
-
92
+ def basic_fields_markup(html, association = nil)
93
93
  if html && html[:class]
94
- html[:class] = 'abyme--fields ' + html[:class]
94
+ html[:class] = "abyme--fields #{association.to_s.singularize}-fields #{html[:class]}"
95
95
  else
96
96
  html ||= {}
97
- html[:class] = 'abyme--fields'
97
+ html[:class] = "abyme--fields #{association.to_s.singularize}-fields"
98
98
  end
99
-
100
- return html
99
+ html
101
100
  end
102
101
 
102
+ def build_attributes(default, attr)
103
+ # ADD NEW DATA ATTRIBUTES VALUES TO THE DEFAULT ONES (ONLY VALUES)
104
+ if attr[:data]
105
+ default[:data].each do |key, value|
106
+ default[:data][key] = "#{value} #{attr[:data][key]}".strip
107
+ end
108
+ # ADD NEW DATA ATTRIBUTES (KEYS & VALUES)
109
+ default[:data] = default[:data].merge(attr[:data].reject { |key, _| default[:data][key] })
110
+ end
111
+ # MERGE THE DATA ATTRIBUTES TO THE HASH OF HTML ATTRIBUTES
112
+ default.merge(attr.reject { |key, _| key == :data })
113
+ end
103
114
  end
104
115
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abyme
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Romain Sanson
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-10-13 00:00:00.000000000 Z
12
+ date: 2020-10-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -60,6 +60,7 @@ executables: []
60
60
  extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
+ - ".DS_Store"
63
64
  - ".gitignore"
64
65
  - ".rspec"
65
66
  - ".travis.yml"
@@ -68,12 +69,13 @@ files:
68
69
  - LICENSE.txt
69
70
  - README.md
70
71
  - Rakefile
71
- - abyme-0.1.2.gem
72
+ - abyme-0.1.3.gem
72
73
  - abyme.gemspec
73
74
  - bin/console
74
75
  - bin/setup
75
76
  - javascript/abyme_controller.js
76
77
  - javascript/index.js
78
+ - lib/.DS_Store
77
79
  - lib/abyme.rb
78
80
  - lib/abyme/abyme_builder.rb
79
81
  - lib/abyme/engine.rb
Binary file