abyme 0.2.2 → 0.5.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.github/workflows/build.yml +60 -0
- data/.gitignore +2 -0
- data/.simplecov +0 -0
- data/CHANGELOG.md +114 -0
- data/Gemfile.lock +162 -28
- data/README.md +119 -180
- data/Rakefile +31 -8
- data/abyme.gemspec +15 -1
- data/javascript/abyme_controller.js +86 -11
- data/lib/abyme.rb +2 -0
- data/lib/abyme/abyme_builder.rb +21 -10
- data/lib/abyme/action_view_extensions/builder.rb +17 -0
- data/lib/abyme/controller.rb +15 -0
- data/lib/abyme/engine.rb +3 -1
- data/lib/abyme/model.rb +81 -5
- data/lib/abyme/version.rb +4 -2
- data/lib/abyme/view_helpers.rb +186 -15
- data/package.json +2 -2
- metadata +148 -7
- data/.travis.yml +0 -7
- data/lib/generators/.DS_Store +0 -0
- data/lib/generators/abyme/install_generator.rb +0 -25
- data/lib/generators/abyme/templates/abyme_controller.js +0 -17
data/abyme.gemspec
CHANGED
@@ -28,7 +28,21 @@ Gem::Specification.new do |spec|
|
|
28
28
|
|
29
29
|
spec.add_development_dependency "bundler", "~> 2.0"
|
30
30
|
spec.add_development_dependency "rake", "~> 13.0"
|
31
|
+
# Tests
|
31
32
|
spec.add_development_dependency "rspec-rails"
|
32
|
-
spec.add_development_dependency
|
33
|
+
spec.add_development_dependency 'rails-controller-testing'
|
34
|
+
spec.add_development_dependency 'database_cleaner-active_record'
|
35
|
+
spec.add_development_dependency 'capybara'
|
36
|
+
spec.add_development_dependency 'webdrivers'
|
37
|
+
spec.add_development_dependency "generator_spec"
|
38
|
+
|
39
|
+
# Dummy app
|
33
40
|
spec.add_development_dependency "sqlite3"
|
41
|
+
spec.add_development_dependency 'rails'
|
42
|
+
spec.add_development_dependency 'pry-rails'
|
43
|
+
spec.add_development_dependency 'web-console'
|
44
|
+
|
45
|
+
spec.add_development_dependency 'puma'
|
46
|
+
spec.add_development_dependency 'simplecov'
|
47
|
+
spec.add_development_dependency 'simplecov-lcov'
|
34
48
|
end
|
@@ -1,27 +1,62 @@
|
|
1
1
|
import { Controller } from 'stimulus';
|
2
2
|
|
3
3
|
export default class extends Controller {
|
4
|
-
static targets = ['template', 'associations', 'fields', 'newFields'];
|
4
|
+
// static targets = ['template', 'associations', 'fields', 'newFields'];
|
5
|
+
// Some applications don't compile correctly with the usual static syntax.
|
6
|
+
// Thus implementing targets with standard getters below
|
7
|
+
|
8
|
+
static get targets() {
|
9
|
+
return ['template', 'associations', 'fields', 'newFields'];
|
10
|
+
}
|
5
11
|
|
6
12
|
connect() {
|
13
|
+
console.log("Abyme Connected")
|
14
|
+
|
7
15
|
if (this.count) {
|
8
|
-
|
16
|
+
// If data-count is present,
|
17
|
+
// add n default fields on page load
|
18
|
+
|
19
|
+
this.add_default_associations();
|
9
20
|
}
|
10
21
|
}
|
11
22
|
|
23
|
+
// return the value of the data-count attribute
|
24
|
+
|
12
25
|
get count() {
|
13
26
|
return this.element.dataset.minCount || 0;
|
14
27
|
}
|
15
28
|
|
29
|
+
// return the value of the data-position attribute
|
30
|
+
// if there is no position specified set end as default
|
31
|
+
|
16
32
|
get position() {
|
17
33
|
return this.associationsTarget.dataset.abymePosition === 'end' ? 'beforeend' : 'afterbegin';
|
18
34
|
}
|
19
35
|
|
36
|
+
// ADD_ASSOCIATION
|
37
|
+
|
38
|
+
// this function is call whenever a click occurs
|
39
|
+
// on the element with the click->abyme#add_association
|
40
|
+
// <button> element by default
|
41
|
+
|
42
|
+
// if a data-count is present the add_association
|
43
|
+
// will be call without an event so we have to check
|
44
|
+
// this case
|
45
|
+
|
46
|
+
// check for limit reached
|
47
|
+
// dispatch an event if the limit is reached
|
48
|
+
|
49
|
+
// - call the function build_html that take care
|
50
|
+
// for building the correct html to be inserted in the DOM
|
51
|
+
// - dispatch an event before insert
|
52
|
+
// - insert html into the dom
|
53
|
+
// - dispatch an event after insert
|
54
|
+
|
20
55
|
add_association(event) {
|
21
56
|
if (event) {
|
22
57
|
event.preventDefault();
|
23
58
|
}
|
24
|
-
|
59
|
+
|
25
60
|
if (this.element.dataset.limit && this.limit_check()) {
|
26
61
|
this.create_event('limit-reached')
|
27
62
|
return false
|
@@ -33,8 +68,21 @@ export default class extends Controller {
|
|
33
68
|
this.create_event('after-add');
|
34
69
|
}
|
35
70
|
|
71
|
+
// REMOVE_ASSOCIATION
|
72
|
+
|
73
|
+
// this function is call whenever a click occurs
|
74
|
+
// on the element with the click->abyme#remove_association
|
75
|
+
// <button> element by default
|
76
|
+
|
77
|
+
// - call the function mark_for_destroy that takes care
|
78
|
+
// of marking the element for destruction and hiding it
|
79
|
+
// - dispatch an event before mark & hide
|
80
|
+
// - mark for descrution + hide the element
|
81
|
+
// - dispatch an event after mark and hide
|
82
|
+
|
36
83
|
remove_association(event) {
|
37
84
|
event.preventDefault();
|
85
|
+
|
38
86
|
this.create_event('before-remove');
|
39
87
|
this.mark_for_destroy(event);
|
40
88
|
this.create_event('after-remove');
|
@@ -42,6 +90,12 @@ export default class extends Controller {
|
|
42
90
|
|
43
91
|
// LIFECYCLE EVENTS RELATED
|
44
92
|
|
93
|
+
// CREATE_EVENT
|
94
|
+
|
95
|
+
// take a stage (String) => before-add, after-add...
|
96
|
+
// create a new custom event
|
97
|
+
// and dispatch at at the controller level
|
98
|
+
|
45
99
|
create_event(stage, html = null) {
|
46
100
|
const event = new CustomEvent(`abyme:${stage}`, { detail: {controller: this, content: html} });
|
47
101
|
this.element.dispatchEvent(event);
|
@@ -69,9 +123,14 @@ export default class extends Controller {
|
|
69
123
|
abymeAfterRemove(event) {
|
70
124
|
}
|
71
125
|
|
72
|
-
//
|
126
|
+
// BUILD HTML
|
127
|
+
|
128
|
+
// takes the html template and substitutes the sub-string
|
129
|
+
// NEW_RECORD for a generated timestamp
|
130
|
+
// then if there is a sub template in the html (multiple nested level)
|
131
|
+
// set all the sub timestamps back as NEW_RECORD
|
132
|
+
// finally returns the html
|
73
133
|
|
74
|
-
// build html
|
75
134
|
build_html() {
|
76
135
|
let html = this.templateTarget.innerHTML.replace(
|
77
136
|
/NEW_RECORD/g,
|
@@ -88,8 +147,15 @@ export default class extends Controller {
|
|
88
147
|
|
89
148
|
return html;
|
90
149
|
}
|
91
|
-
|
92
|
-
//
|
150
|
+
|
151
|
+
// MARK_FOR_DESTROY
|
152
|
+
|
153
|
+
// mark association for destruction
|
154
|
+
// get the closest abyme--fields from the remove_association button
|
155
|
+
// set the _destroy input value as 1
|
156
|
+
// hide the element
|
157
|
+
// add the class of abyme--marked-for-destroy to the element
|
158
|
+
|
93
159
|
mark_for_destroy(event) {
|
94
160
|
let item = event.target.closest('.abyme--fields');
|
95
161
|
item.querySelector("input[name*='_destroy']").value = 1;
|
@@ -97,20 +163,29 @@ export default class extends Controller {
|
|
97
163
|
item.classList.add('abyme--marked-for-destroy')
|
98
164
|
}
|
99
165
|
|
100
|
-
|
166
|
+
|
167
|
+
// LIMIT_CHECK
|
168
|
+
|
169
|
+
// Check if associations limit is reached
|
170
|
+
// based on newFieldsTargets only
|
171
|
+
// persisted fields are ignored
|
172
|
+
|
101
173
|
limit_check() {
|
102
174
|
return (this.newFieldsTargets
|
103
175
|
.filter(item => !item.classList.contains('abyme--marked-for-destroy'))).length
|
104
176
|
>= parseInt(this.element.dataset.limit)
|
105
177
|
}
|
106
178
|
|
107
|
-
//
|
108
|
-
|
179
|
+
// ADD_DEFAULT_ASSOCIATION
|
180
|
+
|
181
|
+
// Add n default blank associations at page load
|
182
|
+
// call sleep function to ensure uniqueness of timestamp
|
183
|
+
|
184
|
+
async add_default_associations() {
|
109
185
|
let i = 0
|
110
186
|
while (i < this.count) {
|
111
187
|
this.add_association()
|
112
188
|
i++
|
113
|
-
// Sleep function to ensure uniqueness of timestamp
|
114
189
|
await this.sleep(1);
|
115
190
|
}
|
116
191
|
}
|
data/lib/abyme.rb
CHANGED
data/lib/abyme/abyme_builder.rb
CHANGED
@@ -2,31 +2,42 @@ module Abyme
|
|
2
2
|
class AbymeBuilder < ActionView::Base
|
3
3
|
include ActionView
|
4
4
|
|
5
|
-
|
5
|
+
# If a block is given to the #abymize helper
|
6
|
+
# it will instanciate a new AbymeBuilder
|
7
|
+
# and pass to it the association name (Symbol)
|
8
|
+
# the form object, lookup_context optionaly a partial path
|
9
|
+
# then yield itself to the block
|
10
|
+
|
11
|
+
def initialize(association:, form:, context:, partial:, &block)
|
6
12
|
@association = association
|
7
13
|
@form = form
|
8
|
-
@
|
14
|
+
@context = context
|
15
|
+
@lookup_context = context.lookup_context
|
9
16
|
@partial = partial
|
10
17
|
yield(self) if block_given?
|
11
18
|
end
|
19
|
+
|
20
|
+
# RECORDS
|
21
|
+
|
22
|
+
# calls the #persisted_records_for helper method
|
23
|
+
# passing association, form and options to it
|
12
24
|
|
13
25
|
def records(options = {})
|
14
26
|
persisted_records_for(@association, @form, options) do |fields_for_association|
|
15
|
-
render_association_partial(fields_for_association,
|
27
|
+
render_association_partial(@association, fields_for_association, @partial, @context)
|
16
28
|
end
|
17
29
|
end
|
30
|
+
|
31
|
+
# NEW_RECORDS
|
32
|
+
|
33
|
+
# calls the #new_records_for helper method
|
34
|
+
# passing association, form and options to it
|
18
35
|
|
19
36
|
def new_records(options = {}, &block)
|
20
37
|
new_records_for(@association, @form, options) do |fields_for_association|
|
21
|
-
render_association_partial(fields_for_association,
|
38
|
+
render_association_partial(@association, fields_for_association, @partial, @context)
|
22
39
|
end
|
23
40
|
end
|
24
41
|
|
25
|
-
private
|
26
|
-
|
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 })
|
30
|
-
end
|
31
42
|
end
|
32
43
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Abyme
|
2
|
+
module ActionViewExtensions
|
3
|
+
module Builder
|
4
|
+
def abyme_for(association, options = {}, &block)
|
5
|
+
@template.abyme_for(association, self, options, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
alias :abymize :abyme_for
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ActionView::Helpers
|
14
|
+
class FormBuilder
|
15
|
+
include Abyme::ActionViewExtensions::Builder
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Abyme
|
2
|
+
module Controller
|
3
|
+
def abyme_attributes
|
4
|
+
return [] if resource_class.nil?
|
5
|
+
|
6
|
+
resource_class.abyme_attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def resource_class
|
12
|
+
self.class.name.match(/(.*)(Controller)/)[1].singularize.safe_constantize
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/abyme/engine.rb
CHANGED
@@ -4,9 +4,11 @@ module Abyme
|
|
4
4
|
|
5
5
|
config.after_initialize do
|
6
6
|
ActiveSupport.on_load :action_view do
|
7
|
-
# ActionView::Base.send :include, Abyme::ViewHelpers
|
8
7
|
include Abyme::ViewHelpers
|
9
8
|
end
|
9
|
+
ActiveSupport.on_load :action_controller do
|
10
|
+
include Abyme::Controller
|
11
|
+
end
|
10
12
|
end
|
11
13
|
end
|
12
14
|
end
|
data/lib/abyme/model.rb
CHANGED
@@ -1,11 +1,87 @@
|
|
1
1
|
module Abyme
|
2
2
|
module Model
|
3
|
-
|
4
|
-
|
5
|
-
class_methods do
|
6
|
-
def abyme_for(association, options = {})
|
3
|
+
module ClassMethods
|
4
|
+
def abymize(association, permit: nil, reject: nil, **options)
|
7
5
|
default_options = {reject_if: :all_blank, allow_destroy: true}
|
8
|
-
|
6
|
+
nested_attributes_options = default_options.merge(options)
|
7
|
+
accepts_nested_attributes_for association, nested_attributes_options
|
8
|
+
# Save allow_destroy value for this model/association for later
|
9
|
+
save_destroy_option(association, nested_attributes_options[:allow_destroy])
|
10
|
+
Abyme::Model.permit_attributes(self.name, association, permit || reject, permit.present?) if permit.present? || reject.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
alias :abyme_for :abymize
|
14
|
+
|
15
|
+
def abyme_attributes
|
16
|
+
Abyme::Model.instance_variable_get(:@permitted_attributes)[self.name]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def save_destroy_option(association, value)
|
22
|
+
Abyme::Model.instance_variable_get(:@allow_destroy)[self.name][association] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@permitted_attributes ||= {}
|
27
|
+
@allow_destroy ||= {}
|
28
|
+
|
29
|
+
attr_accessor :allow_destroy
|
30
|
+
attr_reader :permitted_attributes
|
31
|
+
|
32
|
+
def self.permit_attributes(class_name, association, attributes, permit)
|
33
|
+
@permitted_attributes[class_name]["#{association}_attributes".to_sym] = AttributesBuilder.new(class_name, association, attributes, permit)
|
34
|
+
.build_attributes
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.included(klass)
|
38
|
+
@permitted_attributes[klass.name] ||= {}
|
39
|
+
@allow_destroy[klass.name] ||= {}
|
40
|
+
klass.extend ClassMethods
|
41
|
+
end
|
42
|
+
|
43
|
+
class AttributesBuilder
|
44
|
+
def initialize(model, association, attributes, permit = true)
|
45
|
+
@model = model
|
46
|
+
@association = association
|
47
|
+
@attributes_list = attributes
|
48
|
+
@permit = permit
|
49
|
+
@association_class = @association.to_s.classify.constantize
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_attributes
|
53
|
+
nested_attributes = @association_class.abyme_attributes if @association_class.respond_to? :abyme_attributes
|
54
|
+
authorized_attributes = build_default_attributes
|
55
|
+
if @permit && @attributes_list == :all_attributes
|
56
|
+
authorized_attributes = build_all_attributes(authorized_attributes, nested_attributes)
|
57
|
+
elsif @permit
|
58
|
+
@attributes_list << nested_attributes unless (nested_attributes.blank? || @attributes_list.include?(nested_attributes))
|
59
|
+
authorized_attributes += @attributes_list
|
60
|
+
else
|
61
|
+
authorized_attributes = build_all_attributes(authorized_attributes, nested_attributes)
|
62
|
+
authorized_attributes -= @attributes_list
|
63
|
+
end
|
64
|
+
authorized_attributes
|
65
|
+
end
|
66
|
+
|
67
|
+
def destroy_allowed?
|
68
|
+
Abyme::Model.instance_variable_get(:@allow_destroy).dig(@model, @association)
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_all_attributes
|
72
|
+
@association_class.column_names.map(&:to_sym).reject { |attr| [:id, :created_at, :updated_at].include?(attr) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_all_attributes(authorized_attributes, nested_attributes)
|
76
|
+
authorized_attributes += add_all_attributes
|
77
|
+
authorized_attributes << nested_attributes unless (nested_attributes.blank? || authorized_attributes.include?(nested_attributes))
|
78
|
+
authorized_attributes
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_default_attributes
|
82
|
+
attributes = [:id]
|
83
|
+
attributes << :_destroy if destroy_allowed?
|
84
|
+
attributes
|
9
85
|
end
|
10
86
|
end
|
11
87
|
end
|
data/lib/abyme/version.rb
CHANGED
data/lib/abyme/view_helpers.rb
CHANGED
@@ -1,28 +1,108 @@
|
|
1
|
+
require_relative "abyme_builder"
|
2
|
+
|
1
3
|
module Abyme
|
2
4
|
module ViewHelpers
|
3
5
|
|
4
|
-
|
6
|
+
# ABYME_FOR
|
7
|
+
|
8
|
+
# this helper will generate the top level wrapper markup
|
9
|
+
# with the bare minimum html attributes (data-controller="abyme")
|
10
|
+
# it takes the Symbolized name of the association (plural) and the form object
|
11
|
+
# then you can pass a hash of options (see exemple below)
|
12
|
+
# if no block given it will generate a default markup for
|
13
|
+
# #persisted_records_for, #new_records_for & #add_associated_record methods
|
14
|
+
# if a block is given it will instanciate a new AbymeBuilder and pass to it
|
15
|
+
# the name of the association, the form object and the lookup_context
|
16
|
+
|
17
|
+
# == Options
|
18
|
+
|
19
|
+
# - limit (Integer)
|
20
|
+
# you can set a limit for the new association fields to display
|
21
|
+
|
22
|
+
# - min_count (Integer)
|
23
|
+
# set the default number of blank fields to display
|
24
|
+
|
25
|
+
# - partial (String)
|
26
|
+
# to customize the partial path by default #abyme_for will expect
|
27
|
+
# a partial to bbe present in views/abyme
|
28
|
+
|
29
|
+
# - Exemple
|
30
|
+
|
31
|
+
# <%= abyme_for(:tasks, f, limit: 3) do |abyme| %>
|
32
|
+
# ...
|
33
|
+
# <% end %>
|
34
|
+
|
35
|
+
# will output this html
|
36
|
+
|
37
|
+
# <div data-controller="abyme" data-limit="3" id="abyme--tasks">
|
38
|
+
# ...
|
39
|
+
# </div>
|
40
|
+
|
41
|
+
def abyme_for(association, form, options = {}, &block)
|
5
42
|
content_tag(:div, data: { controller: 'abyme', limit: options[:limit], min_count: options[:min_count] }, id: "abyme--#{association}") do
|
6
43
|
if block_given?
|
7
44
|
yield(Abyme::AbymeBuilder.new(
|
8
|
-
association: association, form: form,
|
45
|
+
association: association, form: form, context: self, partial: options[:partial]
|
9
46
|
)
|
10
47
|
)
|
11
48
|
else
|
12
49
|
model = association.to_s.singularize.classify.constantize
|
13
50
|
concat(persisted_records_for(association, form, options))
|
14
51
|
concat(new_records_for(association, form, options))
|
15
|
-
concat(
|
52
|
+
concat(add_associated_record(content: options[:button_text] || "Add #{model}"))
|
16
53
|
end
|
17
54
|
end
|
18
55
|
end
|
19
56
|
|
57
|
+
alias :abymize :abyme_for
|
58
|
+
|
59
|
+
# NEW_RECORDS_FOR
|
60
|
+
|
61
|
+
# this helper is call by the AbymeBuilder #new_records instance method
|
62
|
+
# it generates the html markup for new associations fields
|
63
|
+
# it takes the association (Symbol) and the form object
|
64
|
+
# then a hash of options.
|
65
|
+
|
66
|
+
# - Exemple
|
67
|
+
# <%= abyme_for(:tasks, f) do |abyme| %>
|
68
|
+
# <%= abyme.new_records %>
|
69
|
+
# ...
|
70
|
+
# <% end %>
|
71
|
+
|
72
|
+
# will output this html
|
73
|
+
|
74
|
+
# <div data-target="abyme.associations" data-association="tasks" data-abyme-position="end">
|
75
|
+
# <template class="abyme--task_template" data-target="abyme.template">
|
76
|
+
# <div data-target="abyme.fields abyme.newFields" class="abyme--fields task-fields">
|
77
|
+
# ... partial html goes here
|
78
|
+
# </div>
|
79
|
+
# </template>
|
80
|
+
# ... new rendered fields goes here
|
81
|
+
# </div>
|
82
|
+
|
83
|
+
# == Options
|
84
|
+
# - position (:start, :end)
|
85
|
+
# allows you to specify whether new fields added dynamically
|
86
|
+
# should go at the top or at the bottom
|
87
|
+
# :end is the default value
|
88
|
+
|
89
|
+
# - partial (String)
|
90
|
+
# to customize the partial path by default #abyme_for will expect
|
91
|
+
# a partial to bbe present in views/abyme
|
92
|
+
|
93
|
+
# - fields_html (Hash)
|
94
|
+
# allows you to pass any html attributes to each fields wrapper
|
95
|
+
|
96
|
+
# - wrapper_html (Hash)
|
97
|
+
# allows you to pass any html attributes to the the html element
|
98
|
+
# wrapping all the fields
|
99
|
+
|
20
100
|
def new_records_for(association, form, options = {}, &block)
|
21
101
|
options[:wrapper_html] ||= {}
|
22
102
|
|
23
103
|
wrapper_default = {
|
24
104
|
data: {
|
25
|
-
|
105
|
+
abyme_target: 'associations',
|
26
106
|
association: association,
|
27
107
|
abyme_position: options[:position] || :end
|
28
108
|
}
|
@@ -31,25 +111,69 @@ module Abyme
|
|
31
111
|
fields_default = { data: { target: 'abyme.fields abyme.newFields' } }
|
32
112
|
|
33
113
|
content_tag(:div, build_attributes(wrapper_default, options[:wrapper_html])) do
|
34
|
-
content_tag(:template, class: "abyme--#{association.to_s.singularize}_template", data: {
|
114
|
+
content_tag(:template, class: "abyme--#{association.to_s.singularize}_template", data: { abyme_target: 'template' }) do
|
35
115
|
form.fields_for association, association.to_s.classify.constantize.new, child_index: 'NEW_RECORD' do |f|
|
36
116
|
content_tag(:div, build_attributes(fields_default, basic_fields_markup(options[:fields_html], association))) do
|
37
117
|
# 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(options[:partial] || "abyme/#{association.to_s.singularize}_fields", f: f)
|
118
|
+
# block_given? ? yield(f) : render(options[:partial] || "abyme/#{association.to_s.singularize}_fields", f: f)
|
119
|
+
block_given? ? yield(f) : render_association_partial(association, f, options[:partial])
|
39
120
|
end
|
40
121
|
end
|
41
122
|
end
|
42
123
|
end
|
43
124
|
end
|
125
|
+
|
126
|
+
# PERSISTED_RECORDS_FOR
|
127
|
+
|
128
|
+
# this helper is call by the AbymeBuilder #records instance method
|
129
|
+
# it generates the html markup for persisted associations fields
|
130
|
+
# it takes the association (Symbol) and the form object
|
131
|
+
# then a hash of options.
|
132
|
+
|
133
|
+
# - Exemple
|
134
|
+
# <%= abyme_for(:tasks, f) do |abyme| %>
|
135
|
+
# <%= abyme.records %>
|
136
|
+
# ...
|
137
|
+
# <% end %>
|
138
|
+
|
139
|
+
# will output this html
|
140
|
+
|
141
|
+
# <div>
|
142
|
+
# <div data-target="abyme.fields" class="abyme--fields task-fields">
|
143
|
+
# ... partial html goes here
|
144
|
+
# </div>
|
145
|
+
# </div>
|
146
|
+
|
147
|
+
# == Options
|
148
|
+
# - collection (Active Record Collection)
|
149
|
+
# allows you to pass an AR collection
|
150
|
+
# by default every associated records will be present
|
151
|
+
|
152
|
+
# - order (Hash)
|
153
|
+
# allows you to order the collection
|
154
|
+
# ex: order: { created_at: :desc }
|
155
|
+
|
156
|
+
# - partial (String)
|
157
|
+
# to customize the partial path by default #abyme_for will expect
|
158
|
+
# a partial to bbe present in views/abyme
|
159
|
+
|
160
|
+
# - fields_html (Hash)
|
161
|
+
# allows you to pass any html attributes to each fields wrapper
|
162
|
+
|
163
|
+
# - wrapper_html (Hash)
|
164
|
+
# allows you to pass any html attributes to the the html element
|
165
|
+
# wrapping all the fields
|
44
166
|
|
45
167
|
def persisted_records_for(association, form, options = {})
|
46
168
|
records = options[:collection] || form.object.send(association)
|
47
169
|
options[:wrapper_html] ||= {}
|
48
|
-
fields_default = { data: {
|
170
|
+
fields_default = { data: { abyme_target: 'fields' } }
|
49
171
|
|
50
172
|
if options[:order].present?
|
51
173
|
records = records.order(options[:order])
|
52
|
-
#
|
174
|
+
# by calling the order method on the AR collection
|
175
|
+
# we get rid of the records with errors
|
176
|
+
# so we have to get them back with the 2 lines below
|
53
177
|
invalids = form.object.send(association).reject(&:persisted?)
|
54
178
|
records = records.to_a.concat(invalids) if invalids.any?
|
55
179
|
end
|
@@ -57,28 +181,54 @@ module Abyme
|
|
57
181
|
content_tag(:div, options[:wrapper_html]) do
|
58
182
|
form.fields_for(association, records) do |f|
|
59
183
|
content_tag(:div, build_attributes(fields_default, basic_fields_markup(options[:fields_html], association))) do
|
60
|
-
block_given? ? yield(f) :
|
184
|
+
block_given? ? yield(f) : render_association_partial(association, f, options[:partial])
|
61
185
|
end
|
62
186
|
end
|
63
187
|
end
|
64
188
|
end
|
189
|
+
|
190
|
+
# ADD & REMOVE ASSOCIATION
|
191
|
+
|
192
|
+
# these helpers will call the #create_button method
|
193
|
+
# to generate the buttons for add and remove associations
|
194
|
+
# with the right action and a default content text for each button
|
65
195
|
|
66
|
-
def
|
196
|
+
def add_associated_record(options = {}, &block)
|
67
197
|
action = 'click->abyme#add_association'
|
198
|
+
options[:content] ||= 'Add Association'
|
68
199
|
create_button(action, options, &block)
|
69
200
|
end
|
70
201
|
|
71
|
-
def
|
202
|
+
def remove_associated_record(options = {}, &block)
|
72
203
|
action = 'click->abyme#remove_association'
|
204
|
+
options[:content] ||= 'Remove Association'
|
73
205
|
create_button(action, options, &block)
|
74
206
|
end
|
75
207
|
|
208
|
+
alias :add_association :add_associated_record
|
209
|
+
alias :remove_association :remove_associated_record
|
210
|
+
|
76
211
|
private
|
212
|
+
|
213
|
+
# CREATE_BUTTON
|
214
|
+
|
215
|
+
# this helper is call by either add_associated_record or remove_associated_record
|
216
|
+
# by default it will generate a button tag.
|
217
|
+
|
218
|
+
# == Options
|
219
|
+
# - content (String)
|
220
|
+
# allows you to set the button text
|
221
|
+
|
222
|
+
# - tag (Symbol)
|
223
|
+
# allows you to set the html tag of your choosing
|
224
|
+
# default if :button
|
225
|
+
|
226
|
+
# - html (Hash)
|
227
|
+
# to pass any html attributes you want.
|
77
228
|
|
78
229
|
def create_button(action, options, &block)
|
79
230
|
options[:html] ||= {}
|
80
231
|
options[:tag] ||= :button
|
81
|
-
options[:content] ||= 'Add Association'
|
82
232
|
|
83
233
|
if block_given?
|
84
234
|
content_tag(options[:tag], { data: { action: action } }.merge(options[:html])) do
|
@@ -89,6 +239,11 @@ module Abyme
|
|
89
239
|
end
|
90
240
|
end
|
91
241
|
|
242
|
+
# BASIC_FIELDS_MARKUP
|
243
|
+
|
244
|
+
# generates the default html classes for fields
|
245
|
+
# add optional classes if present
|
246
|
+
|
92
247
|
def basic_fields_markup(html, association = nil)
|
93
248
|
if html && html[:class]
|
94
249
|
html[:class] = "abyme--fields #{association.to_s.singularize}-fields #{html[:class]}"
|
@@ -99,17 +254,33 @@ module Abyme
|
|
99
254
|
html
|
100
255
|
end
|
101
256
|
|
257
|
+
# BUILD_ATTRIBUTES
|
258
|
+
|
259
|
+
# add optionals html attributes without overwritting
|
260
|
+
# the default or already present ones
|
261
|
+
|
102
262
|
def build_attributes(default, attr)
|
103
|
-
#
|
263
|
+
# Add new data attributes values to the default ones (only values)
|
104
264
|
if attr[:data]
|
105
265
|
default[:data].each do |key, value|
|
106
266
|
default[:data][key] = "#{value} #{attr[:data][key]}".strip
|
107
267
|
end
|
108
|
-
|
268
|
+
# Add new data attributes (keys & values)
|
109
269
|
default[:data] = default[:data].merge(attr[:data].reject { |key, _| default[:data][key] })
|
110
270
|
end
|
111
|
-
#
|
271
|
+
# Merge data attributes to the hash of html attributes
|
112
272
|
default.merge(attr.reject { |key, _| key == :data })
|
113
273
|
end
|
274
|
+
|
275
|
+
# RENDER PARTIAL
|
276
|
+
|
277
|
+
# renders a partial based on the passed path, or will expect a partial to be found in the views/abyme directory.
|
278
|
+
|
279
|
+
def render_association_partial(association, form, partial = nil, context = nil)
|
280
|
+
partial_path = partial ||"abyme/#{association.to_s.singularize}_fields"
|
281
|
+
context ||= self
|
282
|
+
context.render(partial: partial_path, locals: {f: form})
|
283
|
+
end
|
284
|
+
|
114
285
|
end
|
115
286
|
end
|